all: cleanup
General cleanup
This commit is contained in:
parent
a7bd48b64a
commit
086f7836bd
92 changed files with 822 additions and 1084 deletions
|
@ -118,7 +118,7 @@ dependencies {
|
||||||
spotless {
|
spotless {
|
||||||
kotlin {
|
kotlin {
|
||||||
target "src/**/*.kt"
|
target "src/**/*.kt"
|
||||||
ktlint()
|
ktfmt().dropboxStyle()
|
||||||
licenseHeaderFile("NOTICE")
|
licenseHeaderFile("NOTICE")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,11 +50,8 @@ class AuxioApp : Application(), ImageLoaderFactory {
|
||||||
.setIcon(IconCompat.createWithResource(this, R.drawable.ic_shortcut_shuffle_24))
|
.setIcon(IconCompat.createWithResource(this, R.drawable.ic_shortcut_shuffle_24))
|
||||||
.setIntent(
|
.setIntent(
|
||||||
Intent(this, MainActivity::class.java)
|
Intent(this, MainActivity::class.java)
|
||||||
.setAction(INTENT_KEY_SHORTCUT_SHUFFLE)
|
.setAction(INTENT_KEY_SHORTCUT_SHUFFLE))
|
||||||
)
|
.build()))
|
||||||
.build()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newImageLoader() =
|
override fun newImageLoader() =
|
||||||
|
|
|
@ -22,7 +22,6 @@ import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import org.oxycblt.auxio.databinding.ActivityMainBinding
|
import org.oxycblt.auxio.databinding.ActivityMainBinding
|
||||||
|
|
|
@ -31,6 +31,8 @@ import androidx.navigation.fragment.findNavController
|
||||||
import com.google.android.material.bottomsheet.NeoBottomSheetBehavior
|
import com.google.android.material.bottomsheet.NeoBottomSheetBehavior
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import com.google.android.material.transition.MaterialFadeThrough
|
import com.google.android.material.transition.MaterialFadeThrough
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
@ -49,8 +51,6 @@ import org.oxycblt.auxio.util.getAttrColorCompat
|
||||||
import org.oxycblt.auxio.util.getDimen
|
import org.oxycblt.auxio.util.getDimen
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper around the home fragment that shows the playback fragment and controls the more
|
* A wrapper around the home fragment that shows the playback fragment and controls the more
|
||||||
|
@ -89,13 +89,9 @@ class MainFragment :
|
||||||
|
|
||||||
// Send meaningful accessibility events for bottom sheets
|
// Send meaningful accessibility events for bottom sheets
|
||||||
ViewCompat.setAccessibilityPaneTitle(
|
ViewCompat.setAccessibilityPaneTitle(
|
||||||
binding.playbackSheet,
|
binding.playbackSheet, context.getString(R.string.lbl_playback))
|
||||||
context.getString(R.string.lbl_playback)
|
|
||||||
)
|
|
||||||
ViewCompat.setAccessibilityPaneTitle(
|
ViewCompat.setAccessibilityPaneTitle(
|
||||||
binding.queueSheet,
|
binding.queueSheet, context.getString(R.string.lbl_queue))
|
||||||
context.getString(R.string.lbl_queue)
|
|
||||||
)
|
|
||||||
|
|
||||||
val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
|
val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
|
||||||
if (queueSheetBehavior != null) {
|
if (queueSheetBehavior != null) {
|
||||||
|
@ -104,8 +100,7 @@ class MainFragment :
|
||||||
|
|
||||||
unlikelyToBeNull(binding.handleWrapper).setOnClickListener {
|
unlikelyToBeNull(binding.handleWrapper).setOnClickListener {
|
||||||
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED &&
|
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED &&
|
||||||
queueSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED
|
queueSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED) {
|
||||||
) {
|
|
||||||
queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_EXPANDED
|
queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_EXPANDED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,8 +227,7 @@ class MainFragment :
|
||||||
when (action) {
|
when (action) {
|
||||||
is MainNavigationAction.Expand -> tryExpandAll()
|
is MainNavigationAction.Expand -> tryExpandAll()
|
||||||
is MainNavigationAction.Collapse -> tryCollapseAll()
|
is MainNavigationAction.Collapse -> tryCollapseAll()
|
||||||
is MainNavigationAction.Directions ->
|
is MainNavigationAction.Directions -> findNavController().navigate(action.directions)
|
||||||
findNavController().navigate(action.directions)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
navModel.finishMainNavigation()
|
navModel.finishMainNavigation()
|
||||||
|
@ -328,16 +322,14 @@ class MainFragment :
|
||||||
|
|
||||||
if (queueSheetBehavior != null &&
|
if (queueSheetBehavior != null &&
|
||||||
queueSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED &&
|
queueSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED &&
|
||||||
playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED
|
playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED) {
|
||||||
) {
|
|
||||||
// Collapse the queue first if it is expanded.
|
// Collapse the queue first if it is expanded.
|
||||||
queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
|
queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED &&
|
if (playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED &&
|
||||||
playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_HIDDEN
|
playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_HIDDEN) {
|
||||||
) {
|
|
||||||
// Then collapse the playback sheet.
|
// Then collapse the playback sheet.
|
||||||
playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
|
playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
|
||||||
return
|
return
|
||||||
|
@ -357,9 +349,9 @@ class MainFragment :
|
||||||
|
|
||||||
isEnabled =
|
isEnabled =
|
||||||
playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
|
playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
|
||||||
queueSheetBehavior?.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
|
queueSheetBehavior?.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
|
||||||
exploreNavController.currentDestination?.id !=
|
exploreNavController.currentDestination?.id !=
|
||||||
exploreNavController.graph.startDestinationId
|
exploreNavController.graph.startDestinationId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,6 @@ import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import androidx.recyclerview.widget.LinearSmoothScroller
|
import androidx.recyclerview.widget.LinearSmoothScroller
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
import org.oxycblt.auxio.MainFragmentDirections
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
|
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
|
||||||
|
@ -42,7 +41,6 @@ import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.Sort
|
import org.oxycblt.auxio.music.Sort
|
||||||
import org.oxycblt.auxio.music.picker.PickerMode
|
import org.oxycblt.auxio.music.picker.PickerMode
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
|
||||||
import org.oxycblt.auxio.ui.fragment.MusicFragment
|
import org.oxycblt.auxio.ui.fragment.MusicFragment
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.util.canScroll
|
import org.oxycblt.auxio.util.canScroll
|
||||||
|
@ -93,11 +91,7 @@ class AlbumDetailFragment :
|
||||||
collectImmediately(detailModel.currentAlbum, ::handleItemChange)
|
collectImmediately(detailModel.currentAlbum, ::handleItemChange)
|
||||||
collectImmediately(detailModel.albumData, detailAdapter::submitList)
|
collectImmediately(detailModel.albumData, detailAdapter::submitList)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song,
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
playbackModel.parent,
|
|
||||||
playbackModel.isPlaying,
|
|
||||||
::updatePlayback
|
|
||||||
)
|
|
||||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,7 +124,8 @@ class AlbumDetailFragment :
|
||||||
override fun onItemClick(item: Item) {
|
override fun onItemClick(item: Item) {
|
||||||
check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" }
|
check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" }
|
||||||
when (settings.detailPlaybackMode) {
|
when (settings.detailPlaybackMode) {
|
||||||
null, MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
null,
|
||||||
|
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||||
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
|
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
|
||||||
else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}")
|
else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}")
|
||||||
|
@ -233,8 +228,7 @@ class AlbumDetailFragment :
|
||||||
binding.detailRecycler.post {
|
binding.detailRecycler.post {
|
||||||
// Make sure to increment the position to make up for the detail header
|
// Make sure to increment the position to make up for the detail header
|
||||||
binding.detailRecycler.layoutManager?.startSmoothScroll(
|
binding.detailRecycler.layoutManager?.startSmoothScroll(
|
||||||
CenterSmoothScroller(requireContext(), pos)
|
CenterSmoothScroller(requireContext(), pos))
|
||||||
)
|
|
||||||
|
|
||||||
// If the recyclerview can scroll, its certain that it will have to scroll to
|
// If the recyclerview can scroll, its certain that it will have to scroll to
|
||||||
// correctly center the playing item, so make sure that the Toolbar is lifted in
|
// correctly center the playing item, so make sure that the Toolbar is lifted in
|
||||||
|
|
|
@ -26,7 +26,6 @@ import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
import org.oxycblt.auxio.MainFragmentDirections
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
|
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
|
||||||
|
@ -40,7 +39,6 @@ import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.Sort
|
import org.oxycblt.auxio.music.Sort
|
||||||
import org.oxycblt.auxio.music.picker.PickerMode
|
import org.oxycblt.auxio.music.picker.PickerMode
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
|
||||||
import org.oxycblt.auxio.ui.fragment.MusicFragment
|
import org.oxycblt.auxio.ui.fragment.MusicFragment
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.util.collect
|
import org.oxycblt.auxio.util.collect
|
||||||
|
@ -55,7 +53,9 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class ArtistDetailFragment :
|
class ArtistDetailFragment :
|
||||||
MusicFragment<FragmentDetailBinding>(), Toolbar.OnMenuItemClickListener, DetailAdapter.Listener {
|
MusicFragment<FragmentDetailBinding>(),
|
||||||
|
Toolbar.OnMenuItemClickListener,
|
||||||
|
DetailAdapter.Listener {
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
||||||
private val args: ArtistDetailFragmentArgs by navArgs()
|
private val args: ArtistDetailFragmentArgs by navArgs()
|
||||||
|
@ -88,11 +88,7 @@ class ArtistDetailFragment :
|
||||||
collectImmediately(detailModel.currentArtist, ::handleItemChange)
|
collectImmediately(detailModel.currentArtist, ::handleItemChange)
|
||||||
collectImmediately(detailModel.artistData, detailAdapter::submitList)
|
collectImmediately(detailModel.artistData, detailAdapter::submitList)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song,
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
playbackModel.parent,
|
|
||||||
playbackModel.isPlaying,
|
|
||||||
::updatePlayback
|
|
||||||
)
|
|
||||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +118,9 @@ class ArtistDetailFragment :
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> {
|
is Song -> {
|
||||||
when (settings.detailPlaybackMode) {
|
when (settings.detailPlaybackMode) {
|
||||||
null -> playbackModel.playFromArtist(item, unlikelyToBeNull(detailModel.currentArtist.value))
|
null ->
|
||||||
|
playbackModel.playFromArtist(
|
||||||
|
item, unlikelyToBeNull(detailModel.currentArtist.value))
|
||||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||||
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
|
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
|
||||||
|
|
|
@ -29,10 +29,10 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
import java.lang.reflect.Field
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.ui.AuxioAppBarLayout
|
import org.oxycblt.auxio.ui.AuxioAppBarLayout
|
||||||
import org.oxycblt.auxio.util.lazyReflectedField
|
import org.oxycblt.auxio.util.lazyReflectedField
|
||||||
import java.lang.reflect.Field
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An [AuxioAppBarLayout] variant that also shows the name of the toolbar whenever the detail
|
* An [AuxioAppBarLayout] variant that also shows the name of the toolbar whenever the detail
|
||||||
|
@ -145,6 +145,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
private const val TOOLBAR_FADE_DURATION = 150L
|
private const val TOOLBAR_FADE_DURATION = 150L
|
||||||
|
|
||||||
private val TOOLBAR_TITLE_TEXT_FIELD: Field by
|
private val TOOLBAR_TITLE_TEXT_FIELD: Field by
|
||||||
lazyReflectedField(Toolbar::class, "mTitleTextView")
|
lazyReflectedField(Toolbar::class, "mTitleTextView")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -160,11 +160,12 @@ class DetailViewModel(application: Application) :
|
||||||
private fun generateDetailSong(song: Song) {
|
private fun generateDetailSong(song: Song) {
|
||||||
currentSongJob?.cancel()
|
currentSongJob?.cancel()
|
||||||
_currentSong.value = DetailSong(song, null)
|
_currentSong.value = DetailSong(song, null)
|
||||||
currentSongJob = viewModelScope.launch(Dispatchers.IO) {
|
currentSongJob =
|
||||||
val info = generateDetailSongInfo(song)
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
yield()
|
val info = generateDetailSongInfo(song)
|
||||||
_currentSong.value = DetailSong(song, info)
|
yield()
|
||||||
}
|
_currentSong.value = DetailSong(song, info)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateDetailSongInfo(song: Song): SongInfo {
|
private fun generateDetailSongInfo(song: Song): SongInfo {
|
||||||
|
|
|
@ -26,7 +26,6 @@ import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
import org.oxycblt.auxio.MainFragmentDirections
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.detail.recycler.DetailAdapter
|
import org.oxycblt.auxio.detail.recycler.DetailAdapter
|
||||||
|
@ -41,7 +40,6 @@ import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.Sort
|
import org.oxycblt.auxio.music.Sort
|
||||||
import org.oxycblt.auxio.music.picker.PickerMode
|
import org.oxycblt.auxio.music.picker.PickerMode
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
|
||||||
import org.oxycblt.auxio.ui.fragment.MusicFragment
|
import org.oxycblt.auxio.ui.fragment.MusicFragment
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.util.collect
|
import org.oxycblt.auxio.util.collect
|
||||||
|
@ -56,7 +54,9 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class GenreDetailFragment :
|
class GenreDetailFragment :
|
||||||
MusicFragment<FragmentDetailBinding>(), Toolbar.OnMenuItemClickListener, DetailAdapter.Listener {
|
MusicFragment<FragmentDetailBinding>(),
|
||||||
|
Toolbar.OnMenuItemClickListener,
|
||||||
|
DetailAdapter.Listener {
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
||||||
private val args: GenreDetailFragmentArgs by navArgs()
|
private val args: GenreDetailFragmentArgs by navArgs()
|
||||||
|
@ -89,11 +89,7 @@ class GenreDetailFragment :
|
||||||
collectImmediately(detailModel.currentGenre, ::handleItemChange)
|
collectImmediately(detailModel.currentGenre, ::handleItemChange)
|
||||||
collectImmediately(detailModel.genreData, detailAdapter::submitList)
|
collectImmediately(detailModel.genreData, detailAdapter::submitList)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song,
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
playbackModel.parent,
|
|
||||||
playbackModel.isPlaying,
|
|
||||||
::updatePlayback
|
|
||||||
)
|
|
||||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,15 +118,16 @@ class GenreDetailFragment :
|
||||||
override fun onItemClick(item: Item) {
|
override fun onItemClick(item: Item) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Artist -> navModel.exploreNavigateTo(item)
|
is Artist -> navModel.exploreNavigateTo(item)
|
||||||
|
is Song ->
|
||||||
is Song -> when (settings.detailPlaybackMode) {
|
when (settings.detailPlaybackMode) {
|
||||||
null -> playbackModel.playFromGenre(item, unlikelyToBeNull(detailModel.currentGenre.value))
|
null ->
|
||||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
playbackModel.playFromGenre(
|
||||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
item, unlikelyToBeNull(detailModel.currentGenre.value))
|
||||||
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
|
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||||
else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}")
|
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||||
}
|
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
|
||||||
|
else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}")
|
||||||
|
}
|
||||||
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,16 +74,14 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
|
||||||
|
|
||||||
if (song.info.bitrateKbps != null) {
|
if (song.info.bitrateKbps != null) {
|
||||||
binding.detailBitrate.setText(
|
binding.detailBitrate.setText(
|
||||||
getString(R.string.fmt_bitrate, song.info.bitrateKbps)
|
getString(R.string.fmt_bitrate, song.info.bitrateKbps))
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
binding.detailBitrate.setText(R.string.def_bitrate)
|
binding.detailBitrate.setText(R.string.def_bitrate)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (song.info.sampleRate != null) {
|
if (song.info.sampleRate != null) {
|
||||||
binding.detailSampleRate.setText(
|
binding.detailSampleRate.setText(
|
||||||
getString(R.string.fmt_sample_rate, song.info.sampleRate)
|
getString(R.string.fmt_sample_rate, song.info.sampleRate))
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
binding.detailSampleRate.setText(R.string.def_sample_rate)
|
binding.detailSampleRate.setText(R.string.def_sample_rate)
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,9 +119,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailInfo.apply {
|
binding.detailInfo.apply {
|
||||||
val date =
|
val date = item.date?.resolveDate(context) ?: context.getString(R.string.def_date)
|
||||||
item.date?.resolveDate(context)
|
|
||||||
?: context.getString(R.string.def_date)
|
|
||||||
|
|
||||||
val songCount = context.getPlural(R.plurals.fmt_song_count, item.songs.size)
|
val songCount = context.getPlural(R.plurals.fmt_song_count, item.songs.size)
|
||||||
|
|
||||||
|
|
|
@ -118,8 +118,7 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
|
||||||
binding.context.getString(
|
binding.context.getString(
|
||||||
R.string.fmt_two,
|
R.string.fmt_two,
|
||||||
binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size),
|
binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size),
|
||||||
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size)
|
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
|
||||||
)
|
|
||||||
|
|
||||||
binding.detailPlayButton.isVisible = true
|
binding.detailPlayButton.isVisible = true
|
||||||
binding.detailShuffleButton.isVisible = true
|
binding.detailShuffleButton.isVisible = true
|
||||||
|
@ -143,13 +142,14 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
|
||||||
fun new(parent: View) =
|
fun new(parent: View) =
|
||||||
ArtistDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
|
ArtistDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
|
||||||
|
|
||||||
val DIFFER = object : SimpleItemCallback<Artist>() {
|
val DIFFER =
|
||||||
override fun areContentsTheSame(oldItem: Artist, newItem: Artist) =
|
object : SimpleItemCallback<Artist>() {
|
||||||
oldItem.rawName == newItem.rawName &&
|
override fun areContentsTheSame(oldItem: Artist, newItem: Artist) =
|
||||||
oldItem.areGenreContentsTheSame(newItem) &&
|
oldItem.rawName == newItem.rawName &&
|
||||||
oldItem.albums.size == newItem.albums.size &&
|
oldItem.areGenreContentsTheSame(newItem) &&
|
||||||
oldItem.songs.size == newItem.songs.size
|
oldItem.albums.size == newItem.albums.size &&
|
||||||
}
|
oldItem.songs.size == newItem.songs.size
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,8 +159,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
|
||||||
binding.parentImage.bind(item)
|
binding.parentImage.bind(item)
|
||||||
binding.parentName.text = item.resolveName(binding.context)
|
binding.parentName.text = item.resolveName(binding.context)
|
||||||
binding.parentInfo.text =
|
binding.parentInfo.text =
|
||||||
item.date?.resolveDate(binding.context)
|
item.date?.resolveDate(binding.context) ?: binding.context.getString(R.string.def_date)
|
||||||
?: binding.context.getString(R.string.def_date)
|
|
||||||
|
|
||||||
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||||
binding.root.setOnLongClickListener {
|
binding.root.setOnLongClickListener {
|
||||||
|
|
|
@ -35,7 +35,6 @@ import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||||
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
|
|
||||||
abstract class DetailAdapter<L : DetailAdapter.Listener>(
|
abstract class DetailAdapter<L : DetailAdapter.Listener>(
|
||||||
private val listener: L,
|
private val listener: L,
|
||||||
|
@ -43,8 +42,7 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
|
||||||
) : IndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
|
) : IndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
|
||||||
private var isPlaying = false
|
private var isPlaying = false
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis") override fun getItemCount() = differ.currentList.size
|
||||||
override fun getItemCount() = differ.currentList.size
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) =
|
override fun getItemViewType(position: Int) =
|
||||||
when (differ.currentList[position]) {
|
when (differ.currentList[position]) {
|
||||||
|
@ -85,8 +83,7 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
|
||||||
return item is Header || item is SortHeader
|
return item is Header || item is SortHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis") protected val differ = AsyncListDiffer(this, diffCallback)
|
||||||
protected val differ = AsyncListDiffer(this, diffCallback)
|
|
||||||
|
|
||||||
override val currentList: List<Item>
|
override val currentList: List<Item>
|
||||||
get() = differ.currentList
|
get() = differ.currentList
|
||||||
|
|
|
@ -27,9 +27,7 @@ import org.oxycblt.auxio.databinding.ItemDetailBinding
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
|
||||||
import org.oxycblt.auxio.ui.recycler.ArtistViewHolder
|
import org.oxycblt.auxio.ui.recycler.ArtistViewHolder
|
||||||
import org.oxycblt.auxio.ui.recycler.Header
|
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
||||||
import org.oxycblt.auxio.ui.recycler.SongViewHolder
|
import org.oxycblt.auxio.ui.recycler.SongViewHolder
|
||||||
|
@ -105,11 +103,11 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
|
||||||
binding.detailType.text = binding.context.getString(R.string.lbl_genre)
|
binding.detailType.text = binding.context.getString(R.string.lbl_genre)
|
||||||
binding.detailName.text = item.resolveName(binding.context)
|
binding.detailName.text = item.resolveName(binding.context)
|
||||||
binding.detailSubhead.isVisible = false
|
binding.detailSubhead.isVisible = false
|
||||||
binding.detailInfo.text = binding.context.getString(
|
binding.detailInfo.text =
|
||||||
|
binding.context.getString(
|
||||||
R.string.fmt_two,
|
R.string.fmt_two,
|
||||||
binding.context.getPlural(R.plurals.fmt_artist_count, item.artists.size),
|
binding.context.getPlural(R.plurals.fmt_artist_count, item.artists.size),
|
||||||
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size)
|
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
|
||||||
)
|
|
||||||
|
|
||||||
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
|
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
|
||||||
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
|
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
|
||||||
|
|
|
@ -36,6 +36,8 @@ import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
|
import java.lang.reflect.Field
|
||||||
|
import kotlin.math.abs
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.MainFragmentDirections
|
import org.oxycblt.auxio.MainFragmentDirections
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
@ -63,8 +65,6 @@ import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.getColorCompat
|
import org.oxycblt.auxio.util.getColorCompat
|
||||||
import org.oxycblt.auxio.util.lazyReflectedField
|
import org.oxycblt.auxio.util.lazyReflectedField
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import java.lang.reflect.Field
|
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail views for each
|
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail views for each
|
||||||
|
@ -112,8 +112,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
binding.homeToolbar.alpha = 1f - (abs(offset.toFloat()) / (range.toFloat() / 2))
|
binding.homeToolbar.alpha = 1f - (abs(offset.toFloat()) / (range.toFloat() / 2))
|
||||||
|
|
||||||
binding.homeContent.updatePadding(
|
binding.homeContent.updatePadding(
|
||||||
bottom = binding.homeAppbar.totalScrollRange + offset
|
bottom = binding.homeAppbar.totalScrollRange + offset)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,8 +139,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
object : ViewPager2.OnPageChangeCallback() {
|
object : ViewPager2.OnPageChangeCallback() {
|
||||||
override fun onPageSelected(position: Int) =
|
override fun onPageSelected(position: Int) =
|
||||||
homeModel.updateCurrentTab(position)
|
homeModel.updateCurrentTab(position)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
TabLayoutMediator(binding.homeTabs, this, AdaptiveTabStrategy(context, homeModel))
|
TabLayoutMediator(binding.homeTabs, this, AdaptiveTabStrategy(context, homeModel))
|
||||||
.attach()
|
.attach()
|
||||||
|
@ -186,11 +184,13 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
}
|
}
|
||||||
R.id.action_settings -> {
|
R.id.action_settings -> {
|
||||||
logD("Navigating to settings")
|
logD("Navigating to settings")
|
||||||
navModel.mainNavigateTo(MainNavigationAction.Directions(MainFragmentDirections.actionShowSettings()))
|
navModel.mainNavigateTo(
|
||||||
|
MainNavigationAction.Directions(MainFragmentDirections.actionShowSettings()))
|
||||||
}
|
}
|
||||||
R.id.action_about -> {
|
R.id.action_about -> {
|
||||||
logD("Navigating to about")
|
logD("Navigating to about")
|
||||||
navModel.mainNavigateTo(MainNavigationAction.Directions(MainFragmentDirections.actionShowAbout()))
|
navModel.mainNavigateTo(
|
||||||
|
MainNavigationAction.Directions(MainFragmentDirections.actionShowAbout()))
|
||||||
}
|
}
|
||||||
R.id.submenu_sorting -> {
|
R.id.submenu_sorting -> {
|
||||||
// Junk click event when opening the menu
|
// Junk click event when opening the menu
|
||||||
|
@ -200,8 +200,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
homeModel.updateCurrentSort(
|
homeModel.updateCurrentSort(
|
||||||
homeModel
|
homeModel
|
||||||
.getSortForTab(homeModel.currentTab.value)
|
.getSortForTab(homeModel.currentTab.value)
|
||||||
.withAscending(item.isChecked)
|
.withAscending(item.isChecked))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Sorting option was selected, mark it as selected and update the mode
|
// Sorting option was selected, mark it as selected and update the mode
|
||||||
|
@ -209,8 +208,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
homeModel.updateCurrentSort(
|
homeModel.updateCurrentSort(
|
||||||
homeModel
|
homeModel
|
||||||
.getSortForTab(homeModel.currentTab.value)
|
.getSortForTab(homeModel.currentTab.value)
|
||||||
.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId)))
|
.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId))))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,7 +287,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
binding.homeTabs.isVisible = true
|
binding.homeTabs.isVisible = true
|
||||||
toolbarParams.scrollFlags =
|
toolbarParams.scrollFlags =
|
||||||
AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or
|
AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or
|
||||||
AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
|
AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -445,9 +443,9 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val VIEW_PAGER_RECYCLER_FIELD: Field by
|
private val VIEW_PAGER_RECYCLER_FIELD: Field by
|
||||||
lazyReflectedField(ViewPager2::class, "mRecyclerView")
|
lazyReflectedField(ViewPager2::class, "mRecyclerView")
|
||||||
private val VIEW_PAGER_TOUCH_SLOP_FIELD: Field by
|
private val VIEW_PAGER_TOUCH_SLOP_FIELD: Field by
|
||||||
lazyReflectedField(RecyclerView::class, "mTouchSlop")
|
lazyReflectedField(RecyclerView::class, "mTouchSlop")
|
||||||
private const val KEY_LAST_TRANSITION_AXIS =
|
private const val KEY_LAST_TRANSITION_AXIS =
|
||||||
BuildConfig.APPLICATION_ID + ".key.LAST_TRANSITION_AXIS"
|
BuildConfig.APPLICATION_ID + ".key.LAST_TRANSITION_AXIS"
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,13 +142,13 @@ class HomeViewModel(application: Application) :
|
||||||
_songs.value = settings.libSongSort.songs(library.songs)
|
_songs.value = settings.libSongSort.songs(library.songs)
|
||||||
_albums.value = settings.libAlbumSort.albums(library.albums)
|
_albums.value = settings.libAlbumSort.albums(library.albums)
|
||||||
|
|
||||||
_artists.value = settings.libArtistSort.artists(
|
_artists.value =
|
||||||
if (settings.shouldHideCollaborators) {
|
settings.libArtistSort.artists(
|
||||||
library.artists.filter { !it.isCollaborator }
|
if (settings.shouldHideCollaborators) {
|
||||||
} else {
|
library.artists.filter { !it.isCollaborator }
|
||||||
library.artists
|
} else {
|
||||||
}
|
library.artists
|
||||||
)
|
})
|
||||||
|
|
||||||
_genres.value = settings.libGenreSort.genres(library.genres)
|
_genres.value = settings.libGenreSort.genres(library.genres)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import android.os.Bundle
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import java.util.Formatter
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
|
@ -35,7 +36,6 @@ import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||||
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import java.util.Formatter
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [HomeListFragment] for showing a list of [Album]s.
|
* A [HomeListFragment] for showing a list of [Album]s.
|
||||||
|
@ -67,7 +67,8 @@ class AlbumListFragment : HomeListFragment<Album>() {
|
||||||
is Sort.Mode.ByName -> album.collationKey?.run { sourceString.first().uppercase() }
|
is Sort.Mode.ByName -> album.collationKey?.run { sourceString.first().uppercase() }
|
||||||
|
|
||||||
// By Artist -> Use name of first artist
|
// By Artist -> Use name of first artist
|
||||||
is Sort.Mode.ByArtist -> album.artists[0].collationKey?.run { sourceString.first().uppercase() }
|
is Sort.Mode.ByArtist ->
|
||||||
|
album.artists[0].collationKey?.run { sourceString.first().uppercase() }
|
||||||
|
|
||||||
// Year -> Use Full Year
|
// Year -> Use Full Year
|
||||||
is Sort.Mode.ByDate -> album.date?.resolveDate(requireContext())
|
is Sort.Mode.ByDate -> album.date?.resolveDate(requireContext())
|
||||||
|
@ -83,12 +84,11 @@ class AlbumListFragment : HomeListFragment<Album>() {
|
||||||
val dateAddedMillis = album.dateAdded.secsToMs()
|
val dateAddedMillis = album.dateAdded.secsToMs()
|
||||||
formatterSb.setLength(0)
|
formatterSb.setLength(0)
|
||||||
DateUtils.formatDateRange(
|
DateUtils.formatDateRange(
|
||||||
context,
|
context,
|
||||||
formatter,
|
formatter,
|
||||||
dateAddedMillis,
|
dateAddedMillis,
|
||||||
dateAddedMillis,
|
dateAddedMillis,
|
||||||
DateUtils.FORMAT_ABBREV_ALL
|
DateUtils.FORMAT_ABBREV_ALL)
|
||||||
)
|
|
||||||
.toString()
|
.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ import android.os.Bundle
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import org.oxycblt.auxio.MainFragmentDirections
|
import java.util.Formatter
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
|
@ -32,7 +32,6 @@ import org.oxycblt.auxio.music.picker.PickerMode
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.playback.secsToMs
|
import org.oxycblt.auxio.playback.secsToMs
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
|
||||||
import org.oxycblt.auxio.ui.recycler.IndicatorAdapter
|
import org.oxycblt.auxio.ui.recycler.IndicatorAdapter
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||||
|
@ -40,7 +39,6 @@ import org.oxycblt.auxio.ui.recycler.SongViewHolder
|
||||||
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import java.util.Formatter
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [HomeListFragment] for showing a list of [Song]s.
|
* A [HomeListFragment] for showing a list of [Song]s.
|
||||||
|
@ -62,11 +60,7 @@ class SongListFragment : HomeListFragment<Song>() {
|
||||||
|
|
||||||
collectImmediately(homeModel.songs, homeAdapter::replaceList)
|
collectImmediately(homeModel.songs, homeAdapter::replaceList)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song,
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
|
||||||
playbackModel.parent,
|
|
||||||
playbackModel.isPlaying,
|
|
||||||
::handlePlayback
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPopup(pos: Int): String? {
|
override fun getPopup(pos: Int): String? {
|
||||||
|
@ -80,10 +74,12 @@ class SongListFragment : HomeListFragment<Song>() {
|
||||||
is Sort.Mode.ByName -> song.collationKey?.run { sourceString.first().uppercase() }
|
is Sort.Mode.ByName -> song.collationKey?.run { sourceString.first().uppercase() }
|
||||||
|
|
||||||
// Artist -> Use name of first artist
|
// Artist -> Use name of first artist
|
||||||
is Sort.Mode.ByArtist -> song.album.artists[0].collationKey?.run { sourceString.first().uppercase() }
|
is Sort.Mode.ByArtist ->
|
||||||
|
song.album.artists[0].collationKey?.run { sourceString.first().uppercase() }
|
||||||
|
|
||||||
// Album -> Use Album Name
|
// Album -> Use Album Name
|
||||||
is Sort.Mode.ByAlbum -> song.album.collationKey?.run { sourceString.first().uppercase() }
|
is Sort.Mode.ByAlbum ->
|
||||||
|
song.album.collationKey?.run { sourceString.first().uppercase() }
|
||||||
|
|
||||||
// Year -> Use Full Year
|
// Year -> Use Full Year
|
||||||
is Sort.Mode.ByDate -> song.album.date?.resolveDate(requireContext())
|
is Sort.Mode.ByDate -> song.album.date?.resolveDate(requireContext())
|
||||||
|
@ -96,12 +92,11 @@ class SongListFragment : HomeListFragment<Song>() {
|
||||||
val dateAddedMillis = song.dateAdded.secsToMs()
|
val dateAddedMillis = song.dateAdded.secsToMs()
|
||||||
formatterSb.setLength(0)
|
formatterSb.setLength(0)
|
||||||
DateUtils.formatDateRange(
|
DateUtils.formatDateRange(
|
||||||
context,
|
context,
|
||||||
formatter,
|
formatter,
|
||||||
dateAddedMillis,
|
dateAddedMillis,
|
||||||
dateAddedMillis,
|
dateAddedMillis,
|
||||||
DateUtils.FORMAT_ABBREV_ALL
|
DateUtils.FORMAT_ABBREV_ALL)
|
||||||
)
|
|
||||||
.toString()
|
.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,12 +62,7 @@ sealed class Tab(open val mode: MusicMode) {
|
||||||
* Maps between the integer code in the tab sequence and the actual [MusicMode] instance.
|
* Maps between the integer code in the tab sequence and the actual [MusicMode] instance.
|
||||||
*/
|
*/
|
||||||
private val MODE_TABLE =
|
private val MODE_TABLE =
|
||||||
arrayOf(
|
arrayOf(MusicMode.SONGS, MusicMode.ALBUMS, MusicMode.ARTISTS, MusicMode.GENRES)
|
||||||
MusicMode.SONGS,
|
|
||||||
MusicMode.ALBUMS,
|
|
||||||
MusicMode.ARTISTS,
|
|
||||||
MusicMode.GENRES
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Convert an array [tabs] into a sequence of tabs. */
|
/** Convert an array [tabs] into a sequence of tabs. */
|
||||||
fun toSequence(tabs: Array<Tab>): Int {
|
fun toSequence(tabs: Array<Tab>): Int {
|
||||||
|
|
|
@ -82,8 +82,7 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) :
|
||||||
MusicMode.ALBUMS -> R.string.lbl_albums
|
MusicMode.ALBUMS -> R.string.lbl_albums
|
||||||
MusicMode.ARTISTS -> R.string.lbl_artists
|
MusicMode.ARTISTS -> R.string.lbl_artists
|
||||||
MusicMode.GENRES -> R.string.lbl_genres
|
MusicMode.GENRES -> R.string.lbl_genres
|
||||||
}
|
})
|
||||||
)
|
|
||||||
isChecked = item is Tab.Visible
|
isChecked = item is Tab.Visible
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -88,8 +88,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
|
||||||
when (tab) {
|
when (tab) {
|
||||||
is Tab.Visible -> Tab.Invisible(tab.mode)
|
is Tab.Visible -> Tab.Invisible(tab.mode)
|
||||||
is Tab.Invisible -> Tab.Visible(tab.mode)
|
is Tab.Invisible -> Tab.Visible(tab.mode)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =
|
(requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =
|
||||||
|
|
|
@ -51,9 +51,7 @@ class BitmapProvider(private val context: Context) {
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun load(song: Song, target: Target) {
|
fun load(song: Song, target: Target) {
|
||||||
val handle = synchronized(handleLock) {
|
val handle = synchronized(handleLock) { ++currentHandle }
|
||||||
++currentHandle
|
|
||||||
}
|
|
||||||
|
|
||||||
currentRequest?.run { disposable.dispose() }
|
currentRequest?.run { disposable.dispose() }
|
||||||
currentRequest = null
|
currentRequest = null
|
||||||
|
@ -77,10 +75,8 @@ class BitmapProvider(private val context: Context) {
|
||||||
target.onCompleted(null)
|
target.onCompleted(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
.transformations(SquareFrameTransform.INSTANCE))
|
||||||
.transformations(SquareFrameTransform.INSTANCE)
|
|
||||||
)
|
|
||||||
|
|
||||||
currentRequest = Request(context.imageLoader.enqueue(request.build()), target)
|
currentRequest = Request(context.imageLoader.enqueue(request.build()), target)
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,18 +28,21 @@ enum class CoverMode {
|
||||||
MEDIA_STORE,
|
MEDIA_STORE,
|
||||||
QUALITY;
|
QUALITY;
|
||||||
|
|
||||||
val intCode: Int get() = when (this) {
|
val intCode: Int
|
||||||
OFF -> IntegerTable.COVER_MODE_OFF
|
get() =
|
||||||
MEDIA_STORE -> IntegerTable.COVER_MODE_MEDIA_STORE
|
when (this) {
|
||||||
QUALITY -> IntegerTable.COVER_MODE_QUALITY
|
OFF -> IntegerTable.COVER_MODE_OFF
|
||||||
}
|
MEDIA_STORE -> IntegerTable.COVER_MODE_MEDIA_STORE
|
||||||
|
QUALITY -> IntegerTable.COVER_MODE_QUALITY
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromIntCode(intCode: Int) = when (intCode) {
|
fun fromIntCode(intCode: Int) =
|
||||||
IntegerTable.COVER_MODE_OFF -> OFF
|
when (intCode) {
|
||||||
IntegerTable.COVER_MODE_MEDIA_STORE -> MEDIA_STORE
|
IntegerTable.COVER_MODE_OFF -> OFF
|
||||||
IntegerTable.COVER_MODE_QUALITY -> QUALITY
|
IntegerTable.COVER_MODE_MEDIA_STORE -> MEDIA_STORE
|
||||||
else -> null
|
IntegerTable.COVER_MODE_QUALITY -> QUALITY
|
||||||
}
|
else -> null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,11 +26,11 @@ import androidx.annotation.AttrRes
|
||||||
import androidx.appcompat.widget.AppCompatImageView
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
import androidx.core.widget.ImageViewCompat
|
import androidx.core.widget.ImageViewCompat
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
|
import kotlin.math.max
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.getColorCompat
|
import org.oxycblt.auxio.util.getColorCompat
|
||||||
import org.oxycblt.auxio.util.getDrawableCompat
|
import org.oxycblt.auxio.util.getDrawableCompat
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View that displays the playback indicator. Nominally emulates [StyledImageView], but is much
|
* View that displays the playback indicator. Nominally emulates [StyledImageView], but is much
|
||||||
|
@ -111,21 +111,15 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
0f,
|
0f,
|
||||||
0f,
|
0f,
|
||||||
drawable.intrinsicWidth.toFloat(),
|
drawable.intrinsicWidth.toFloat(),
|
||||||
drawable.intrinsicHeight.toFloat()
|
drawable.intrinsicHeight.toFloat())
|
||||||
)
|
|
||||||
indicatorMatrixDst.set(0f, 0f, iconSize.toFloat(), iconSize.toFloat())
|
indicatorMatrixDst.set(0f, 0f, iconSize.toFloat(), iconSize.toFloat())
|
||||||
indicatorMatrix.setRectToRect(
|
indicatorMatrix.setRectToRect(
|
||||||
indicatorMatrixSrc,
|
indicatorMatrixSrc, indicatorMatrixDst, Matrix.ScaleToFit.CENTER)
|
||||||
indicatorMatrixDst,
|
|
||||||
Matrix.ScaleToFit.CENTER
|
|
||||||
)
|
|
||||||
|
|
||||||
// Then actually center it into the icon, which the previous call does not
|
// Then actually center it into the icon, which the previous call does not
|
||||||
// actually do.
|
// actually do.
|
||||||
indicatorMatrix.postTranslate(
|
indicatorMatrix.postTranslate(
|
||||||
(measuredWidth - iconSize) / 2f,
|
(measuredWidth - iconSize) / 2f, (measuredHeight - iconSize) / 2f)
|
||||||
(measuredHeight - iconSize) / 2f
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,9 +95,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.StyledImageView)
|
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.StyledImageView)
|
||||||
val staticIcon =
|
val staticIcon =
|
||||||
styledAttrs.getResourceId(
|
styledAttrs.getResourceId(
|
||||||
R.styleable.StyledImageView_staticIcon,
|
R.styleable.StyledImageView_staticIcon, ResourcesCompat.ID_NULL)
|
||||||
ResourcesCompat.ID_NULL
|
|
||||||
)
|
|
||||||
if (staticIcon != ResourcesCompat.ID_NULL) {
|
if (staticIcon != ResourcesCompat.ID_NULL) {
|
||||||
this.staticIcon = context.getDrawableCompat(staticIcon)
|
this.staticIcon = context.getDrawableCompat(staticIcon)
|
||||||
}
|
}
|
||||||
|
@ -146,8 +144,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
adjustWidth,
|
adjustWidth,
|
||||||
adjustHeight,
|
adjustHeight,
|
||||||
bounds.width() - adjustWidth,
|
bounds.width() - adjustWidth,
|
||||||
bounds.height() - adjustHeight
|
bounds.height() - adjustHeight)
|
||||||
)
|
|
||||||
src.draw(canvas)
|
src.draw(canvas)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
|
import android.util.Size as AndroidSize
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
import coil.decode.DataSource
|
import coil.decode.DataSource
|
||||||
import coil.decode.ImageSource
|
import coil.decode.ImageSource
|
||||||
|
@ -37,6 +38,8 @@ import com.google.android.exoplayer2.MediaMetadata
|
||||||
import com.google.android.exoplayer2.MetadataRetriever
|
import com.google.android.exoplayer2.MetadataRetriever
|
||||||
import com.google.android.exoplayer2.metadata.flac.PictureFrame
|
import com.google.android.exoplayer2.metadata.flac.PictureFrame
|
||||||
import com.google.android.exoplayer2.metadata.id3.ApicFrame
|
import com.google.android.exoplayer2.metadata.id3.ApicFrame
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.InputStream
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
|
@ -46,9 +49,6 @@ import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.InputStream
|
|
||||||
import android.util.Size as AndroidSize
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base implementation for all image fetchers in Auxio.
|
* The base implementation for all image fetchers in Auxio.
|
||||||
|
@ -58,8 +58,8 @@ import android.util.Size as AndroidSize
|
||||||
*/
|
*/
|
||||||
abstract class BaseFetcher : Fetcher {
|
abstract class BaseFetcher : Fetcher {
|
||||||
/**
|
/**
|
||||||
* Fetch the [album] cover. This call respects user configuration and has proper
|
* Fetch the [album] cover. This call respects user configuration and has proper redundancy in
|
||||||
* redundancy in the case that metadata fails to load.
|
* the case that metadata fails to load.
|
||||||
*/
|
*/
|
||||||
protected suspend fun fetchCover(context: Context, album: Album): InputStream? {
|
protected suspend fun fetchCover(context: Context, album: Album): InputStream? {
|
||||||
val settings = Settings(context)
|
val settings = Settings(context)
|
||||||
|
@ -191,8 +191,7 @@ abstract class BaseFetcher : Fetcher {
|
||||||
return SourceResult(
|
return SourceResult(
|
||||||
source = ImageSource(stream.source().buffer(), context),
|
source = ImageSource(stream.source().buffer(), context),
|
||||||
mimeType = null,
|
mimeType = null,
|
||||||
dataSource = DataSource.DISK
|
dataSource = DataSource.DISK)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,9 +220,7 @@ abstract class BaseFetcher : Fetcher {
|
||||||
// resolution.
|
// resolution.
|
||||||
val bitmap =
|
val bitmap =
|
||||||
SquareFrameTransform.INSTANCE.transform(
|
SquareFrameTransform.INSTANCE.transform(
|
||||||
BitmapFactory.decodeStream(stream),
|
BitmapFactory.decodeStream(stream), mosaicFrameSize)
|
||||||
mosaicFrameSize
|
|
||||||
)
|
|
||||||
|
|
||||||
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
|
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
|
||||||
|
|
||||||
|
@ -241,8 +238,7 @@ abstract class BaseFetcher : Fetcher {
|
||||||
return DrawableResult(
|
return DrawableResult(
|
||||||
drawable = mosaicBitmap.toDrawable(context.resources),
|
drawable = mosaicBitmap.toDrawable(context.resources),
|
||||||
isSampled = true,
|
isSampled = true,
|
||||||
dataSource = DataSource.DISK
|
dataSource = DataSource.DISK)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Dimension.mosaicSize(): Int {
|
private fun Dimension.mosaicSize(): Int {
|
||||||
|
|
|
@ -27,6 +27,7 @@ import coil.fetch.SourceResult
|
||||||
import coil.key.Keyer
|
import coil.key.Keyer
|
||||||
import coil.request.Options
|
import coil.request.Options
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
|
import kotlin.math.min
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.source
|
import okio.source
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
|
@ -35,7 +36,6 @@ import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.Sort
|
import org.oxycblt.auxio.music.Sort
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
/** A basic keyer for music data. */
|
/** A basic keyer for music data. */
|
||||||
class MusicKeyer : Keyer<Music> {
|
class MusicKeyer : Keyer<Music> {
|
||||||
|
@ -60,8 +60,7 @@ private constructor(private val context: Context, private val album: Album) : Ba
|
||||||
SourceResult(
|
SourceResult(
|
||||||
source = ImageSource(stream.source().buffer(), context),
|
source = ImageSource(stream.source().buffer(), context),
|
||||||
mimeType = null,
|
mimeType = null,
|
||||||
dataSource = DataSource.DISK
|
dataSource = DataSource.DISK)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SongFactory : Fetcher.Factory<Song> {
|
class SongFactory : Fetcher.Factory<Song> {
|
||||||
|
|
|
@ -21,6 +21,11 @@ package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.text.CollationKey
|
||||||
|
import java.text.Collator
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.math.max
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
@ -36,11 +41,6 @@ import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.text.CollationKey
|
|
||||||
import java.text.Collator
|
|
||||||
import java.util.UUID
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
// --- MUSIC MODELS ---
|
// --- MUSIC MODELS ---
|
||||||
|
|
||||||
|
@ -55,14 +55,14 @@ sealed class Music : Item {
|
||||||
abstract val rawSortName: String?
|
abstract val rawSortName: String?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A key used by the sorting system that takes into account the sort tags of this item,
|
* A key used by the sorting system that takes into account the sort tags of this item, any
|
||||||
* any (english) articles that prefix the names, and collation rules.
|
* (english) articles that prefix the names, and collation rules.
|
||||||
*/
|
*/
|
||||||
abstract val collationKey: CollationKey?
|
abstract val collationKey: CollationKey?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a name from it's raw form to a form suitable to be shown in a UI.
|
* Resolve a name from it's raw form to a form suitable to be shown in a UI. Null values will be
|
||||||
* Null values will be resolved into their string form with this function.
|
* resolved into their string form with this function.
|
||||||
*/
|
*/
|
||||||
abstract fun resolveName(context: Context): String
|
abstract fun resolveName(context: Context): String
|
||||||
|
|
||||||
|
@ -75,26 +75,27 @@ sealed class Music : Item {
|
||||||
other is Music && javaClass == other.javaClass && uid == other.uid
|
other is Music && javaClass == other.javaClass && uid == other.uid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Workaround to allow for easy collation key generation in the initializer without
|
* Workaround to allow for easy collation key generation in the initializer without base-class
|
||||||
* base-class initialization issues or slow lazy initialization.
|
* initialization issues or slow lazy initialization.
|
||||||
*/
|
*/
|
||||||
protected fun makeCollationKeyImpl(): CollationKey? {
|
protected fun makeCollationKeyImpl(): CollationKey? {
|
||||||
val sortName = (rawSortName ?: rawName)?.run {
|
val sortName =
|
||||||
when {
|
(rawSortName ?: rawName)?.run {
|
||||||
length > 5 && startsWith("the ", ignoreCase = true) -> substring(4)
|
when {
|
||||||
length > 4 && startsWith("an ", ignoreCase = true) -> substring(3)
|
length > 5 && startsWith("the ", ignoreCase = true) -> substring(4)
|
||||||
length > 3 && startsWith("a ", ignoreCase = true) -> substring(2)
|
length > 4 && startsWith("an ", ignoreCase = true) -> substring(3)
|
||||||
else -> this
|
length > 3 && startsWith("a ", ignoreCase = true) -> substring(2)
|
||||||
|
else -> this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return COLLATOR.getCollationKey(sortName)
|
return COLLATOR.getCollationKey(sortName)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the library has been linked and validation/construction steps dependent
|
* Called when the library has been linked and validation/construction steps dependent on linked
|
||||||
* on linked items should run. It's also used to do last-step initialization of fields
|
* items should run. It's also used to do last-step initialization of fields that require any
|
||||||
* that require any parent values that would not be present during startup.
|
* parent values that would not be present during startup.
|
||||||
*/
|
*/
|
||||||
abstract fun _finalize()
|
abstract fun _finalize()
|
||||||
|
|
||||||
|
@ -107,15 +108,16 @@ sealed class Music : Item {
|
||||||
* external sources, as it can persist across app restarts and does not need to encode useless
|
* external sources, as it can persist across app restarts and does not need to encode useless
|
||||||
* information about the relationships between items.
|
* information about the relationships between items.
|
||||||
*
|
*
|
||||||
* Note: While the core of a UID is a UUID. The whole is not technically a UUID, with
|
* Note: While the core of a UID is a UUID. The whole is not technically a UUID, with string
|
||||||
* string representation in particular having multiple extensions to increase uniqueness.
|
* representation in particular having multiple extensions to increase uniqueness. Please don't
|
||||||
* Please don't try to do anything interesting with this and just assume it's a black box
|
* try to do anything interesting with this and just assume it's a black box that can only be
|
||||||
* that can only be compared, serialized, and deserialized.
|
* compared, serialized, and deserialized.
|
||||||
*
|
*
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class UID private constructor(
|
class UID
|
||||||
|
private constructor(
|
||||||
private val format: Format,
|
private val format: Format,
|
||||||
private val mode: MusicMode,
|
private val mode: MusicMode,
|
||||||
private val uuid: UUID
|
private val uuid: UUID
|
||||||
|
@ -130,9 +132,8 @@ sealed class Music : Item {
|
||||||
|
|
||||||
override fun hashCode() = hashCode
|
override fun hashCode() = hashCode
|
||||||
|
|
||||||
override fun equals(other: Any?) = other is UID &&
|
override fun equals(other: Any?) =
|
||||||
format == other.format &&
|
other is UID && format == other.format && mode == other.mode && uuid == other.uuid
|
||||||
mode == other.mode && uuid == other.uuid
|
|
||||||
|
|
||||||
// UID string format is roughly:
|
// UID string format is roughly:
|
||||||
// format_namespace:music_mode_int-uuid
|
// format_namespace:music_mode_int-uuid
|
||||||
|
@ -151,11 +152,12 @@ sealed class Music : Item {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val format = when (split[0]) {
|
val format =
|
||||||
Format.AUXIO.namespace -> Format.AUXIO
|
when (split[0]) {
|
||||||
Format.MUSICBRAINZ.namespace -> Format.MUSICBRAINZ
|
Format.AUXIO.namespace -> Format.AUXIO
|
||||||
else -> return null
|
Format.MUSICBRAINZ.namespace -> Format.MUSICBRAINZ
|
||||||
}
|
else -> return null
|
||||||
|
}
|
||||||
|
|
||||||
val ids = split[1].split('-', limit = 2)
|
val ids = split[1].split('-', limit = 2)
|
||||||
if (ids.size != 2) {
|
if (ids.size != 2) {
|
||||||
|
@ -168,9 +170,7 @@ sealed class Music : Item {
|
||||||
return UID(format, mode, uuid)
|
return UID(format, mode, uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Make a UUID derived from the MD5 hash of the data digested in [updates]. */
|
||||||
* Make a UUID derived from the MD5 hash of the data digested in [updates].
|
|
||||||
*/
|
|
||||||
fun auxio(mode: MusicMode, updates: MessageDigest.() -> Unit): UID {
|
fun auxio(mode: MusicMode, updates: MessageDigest.() -> Unit): UID {
|
||||||
// Auxio hashes consist of the MD5 hash of the non-subjective, consistent
|
// Auxio hashes consist of the MD5 hash of the non-subjective, consistent
|
||||||
// tags in a music item. For easier use with MusicBrainz IDs, we transform
|
// tags in a music item. For easier use with MusicBrainz IDs, we transform
|
||||||
|
@ -181,18 +181,13 @@ sealed class Music : Item {
|
||||||
return UID(Format.AUXIO, mode, uuid)
|
return UID(Format.AUXIO, mode, uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Make a UUID derived from a MusicBrainz ID. */
|
||||||
* Make a UUID derived from a MusicBrainz ID.
|
fun musicBrainz(mode: MusicMode, uuid: UUID): UID = UID(Format.MUSICBRAINZ, mode, uuid)
|
||||||
*/
|
|
||||||
fun musicBrainz(mode: MusicMode, uuid: UUID): UID =
|
|
||||||
UID(Format.MUSICBRAINZ, mode, uuid)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val COLLATOR = Collator.getInstance().apply {
|
private val COLLATOR = Collator.getInstance().apply { strength = Collator.PRIMARY }
|
||||||
strength = Collator.PRIMARY
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,7 +202,10 @@ sealed class MusicParent : Music() {
|
||||||
override fun hashCode() = 31 * uid.hashCode() + songs.hashCode()
|
override fun hashCode() = 31 * uid.hashCode() + songs.hashCode()
|
||||||
|
|
||||||
override fun equals(other: Any?) =
|
override fun equals(other: Any?) =
|
||||||
other is MusicParent && javaClass == other.javaClass && uid == other.uid && songs == other.songs
|
other is MusicParent &&
|
||||||
|
javaClass == other.javaClass &&
|
||||||
|
uid == other.uid &&
|
||||||
|
songs == other.songs
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -215,21 +213,22 @@ sealed class MusicParent : Music() {
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class Song constructor(raw: Raw, settings: Settings) : Music() {
|
class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
override val uid = raw.musicBrainzId?.toUuidOrNull()?.let { UID.musicBrainz(MusicMode.SONGS, it) }
|
override val uid =
|
||||||
?: UID.auxio(MusicMode.SONGS) {
|
raw.musicBrainzId?.toUuidOrNull()?.let { UID.musicBrainz(MusicMode.SONGS, it) }
|
||||||
// Song UIDs are based on the raw data without parsing so that they remain
|
?: UID.auxio(MusicMode.SONGS) {
|
||||||
// consistent across music setting changes. Parents are not held up to the
|
// Song UIDs are based on the raw data without parsing so that they remain
|
||||||
// same standard since grouping is already inherently linked to settings.
|
// consistent across music setting changes. Parents are not held up to the
|
||||||
update(raw.name)
|
// same standard since grouping is already inherently linked to settings.
|
||||||
update(raw.albumName)
|
update(raw.name)
|
||||||
update(raw.date)
|
update(raw.albumName)
|
||||||
|
update(raw.date)
|
||||||
|
|
||||||
update(raw.track)
|
update(raw.track)
|
||||||
update(raw.disc)
|
update(raw.disc)
|
||||||
|
|
||||||
update(raw.artistNames)
|
update(raw.artistNames)
|
||||||
update(raw.albumArtistNames)
|
update(raw.albumArtistNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val rawName = requireNotNull(raw.name) { "Invalid raw: No title" }
|
override val rawName = requireNotNull(raw.name) { "Invalid raw: No title" }
|
||||||
|
|
||||||
|
@ -258,15 +257,13 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
val path =
|
val path =
|
||||||
Path(
|
Path(
|
||||||
name = requireNotNull(raw.fileName) { "Invalid raw: No display name" },
|
name = requireNotNull(raw.fileName) { "Invalid raw: No display name" },
|
||||||
parent = requireNotNull(raw.directory) { "Invalid raw: No parent directory" }
|
parent = requireNotNull(raw.directory) { "Invalid raw: No parent directory" })
|
||||||
)
|
|
||||||
|
|
||||||
/** The mime type of the audio file. Only intended for display. */
|
/** The mime type of the audio file. Only intended for display. */
|
||||||
val mimeType =
|
val mimeType =
|
||||||
MimeType(
|
MimeType(
|
||||||
fromExtension = requireNotNull(raw.extensionMimeType) { "Invalid raw: No mime type" },
|
fromExtension = requireNotNull(raw.extensionMimeType) { "Invalid raw: No mime type" },
|
||||||
fromFormat = raw.formatMimeType
|
fromFormat = raw.formatMimeType)
|
||||||
)
|
|
||||||
|
|
||||||
/** The size of this audio file. */
|
/** The size of this audio file. */
|
||||||
val size = requireNotNull(raw.size) { "Invalid raw: No size" }
|
val size = requireNotNull(raw.size) { "Invalid raw: No size" }
|
||||||
|
@ -280,8 +277,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
private var _album: Album? = null
|
private var _album: Album? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The album of this song. Every song is guaranteed to have one and only one album,
|
* The album of this song. Every song is guaranteed to have one and only one album, with a
|
||||||
* with a "directory" album being used if no album tag can be found.
|
* "directory" album being used if no album tag can be found.
|
||||||
*/
|
*/
|
||||||
val album: Album
|
val album: Album
|
||||||
get() = unlikelyToBeNull(_album)
|
get() = unlikelyToBeNull(_album)
|
||||||
|
@ -298,34 +295,41 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
|
|
||||||
private val albumArtistSortNames = raw.albumArtistSortNames.parseMultiValue(settings)
|
private val albumArtistSortNames = raw.albumArtistSortNames.parseMultiValue(settings)
|
||||||
|
|
||||||
private val rawArtists = artistNames.mapIndexed { i, name ->
|
private val rawArtists =
|
||||||
Artist.Raw(artistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(), name, artistSortNames.getOrNull(i))
|
artistNames.mapIndexed { i, name ->
|
||||||
}
|
Artist.Raw(
|
||||||
|
artistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(),
|
||||||
|
name,
|
||||||
|
artistSortNames.getOrNull(i))
|
||||||
|
}
|
||||||
|
|
||||||
private val rawAlbumArtists = albumArtistNames.mapIndexed { i, name ->
|
private val rawAlbumArtists =
|
||||||
Artist.Raw(albumArtistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(), name, albumArtistSortNames.getOrNull(i))
|
albumArtistNames.mapIndexed { i, name ->
|
||||||
}
|
Artist.Raw(
|
||||||
|
albumArtistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(),
|
||||||
|
name,
|
||||||
|
albumArtistSortNames.getOrNull(i))
|
||||||
|
}
|
||||||
|
|
||||||
private val _artists = mutableListOf<Artist>()
|
private val _artists = mutableListOf<Artist>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The artists of this song. Most often one, but there could be multiple. These artists
|
* The artists of this song. Most often one, but there could be multiple. These artists are
|
||||||
* are derived from the artists tag and not the album artists tag, so they may differ from
|
* derived from the artists tag and not the album artists tag, so they may differ from the
|
||||||
* the artists of the album.
|
* artists of the album.
|
||||||
*/
|
*/
|
||||||
val artists: List<Artist>
|
val artists: List<Artist>
|
||||||
get() = _artists
|
get() = _artists
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the artists of this song into a human-readable name. First tries to use artist
|
* Resolve the artists of this song into a human-readable name. First tries to use artist tags,
|
||||||
* tags, then falls back to album artist tags.
|
* then falls back to album artist tags.
|
||||||
*/
|
*/
|
||||||
fun resolveArtistContents(context: Context) =
|
fun resolveArtistContents(context: Context) = artists.joinToString { it.resolveName(context) }
|
||||||
artists.joinToString { it.resolveName(context) }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility method for recyclerview diffing that checks if resolveArtistContents is the
|
* Utility method for recyclerview diffing that checks if resolveArtistContents is the same
|
||||||
* same without a context.
|
* without a context.
|
||||||
*/
|
*/
|
||||||
fun areArtistContentsTheSame(other: Song): Boolean {
|
fun areArtistContentsTheSame(other: Song): Boolean {
|
||||||
for (i in 0 until max(artists.size, other.artists.size)) {
|
for (i in 0 until max(artists.size, other.artists.size)) {
|
||||||
|
@ -348,19 +352,18 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
val genres: List<Genre>
|
val genres: List<Genre>
|
||||||
get() = _genres
|
get() = _genres
|
||||||
|
|
||||||
/**
|
/** Resolve the genres of the song into a human-readable string. */
|
||||||
* Resolve the genres of the song into a human-readable string.
|
|
||||||
*/
|
|
||||||
fun resolveGenreContents(context: Context) = genres.joinToString { it.resolveName(context) }
|
fun resolveGenreContents(context: Context) = genres.joinToString { it.resolveName(context) }
|
||||||
|
|
||||||
// --- INTERNAL FIELDS ---
|
// --- INTERNAL FIELDS ---
|
||||||
|
|
||||||
val _rawGenres = raw.genreNames.parseId3GenreNames(settings)
|
val _rawGenres =
|
||||||
.map { Genre.Raw(it) }.ifEmpty { listOf(Genre.Raw()) }
|
raw.genreNames
|
||||||
|
.parseId3GenreNames(settings)
|
||||||
|
.map { Genre.Raw(it) }
|
||||||
|
.ifEmpty { listOf(Genre.Raw()) }
|
||||||
|
|
||||||
val _rawArtists = rawArtists.ifEmpty { rawAlbumArtists }.ifEmpty {
|
val _rawArtists = rawArtists.ifEmpty { rawAlbumArtists }.ifEmpty { listOf(Artist.Raw()) }
|
||||||
listOf(Artist.Raw())
|
|
||||||
}
|
|
||||||
|
|
||||||
val _rawAlbum =
|
val _rawAlbum =
|
||||||
Album.Raw(
|
Album.Raw(
|
||||||
|
@ -369,8 +372,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
|
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
|
||||||
sortName = raw.albumSortName,
|
sortName = raw.albumSortName,
|
||||||
releaseType = ReleaseType.parse(raw.albumReleaseTypes.parseMultiValue(settings)),
|
releaseType = ReleaseType.parse(raw.albumReleaseTypes.parseMultiValue(settings)),
|
||||||
rawArtists = rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) }
|
rawArtists =
|
||||||
)
|
rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) })
|
||||||
|
|
||||||
fun _link(album: Album) {
|
fun _link(album: Album) {
|
||||||
_album = album
|
_album = album
|
||||||
|
@ -388,7 +391,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
checkNotNull(_album) { "Malformed song: No album" }
|
checkNotNull(_album) { "Malformed song: No album" }
|
||||||
|
|
||||||
check(_artists.isNotEmpty()) { "Malformed song: No artists" }
|
check(_artists.isNotEmpty()) { "Malformed song: No artists" }
|
||||||
for( i in _artists.indices ) {
|
for (i in _artists.indices) {
|
||||||
// Non-destructively reorder the linked artists so that they align with
|
// Non-destructively reorder the linked artists so that they align with
|
||||||
// the artist ordering within the song metadata.
|
// the artist ordering within the song metadata.
|
||||||
val newIdx = _artists[i]._getOriginalPositionIn(_rawArtists)
|
val newIdx = _artists[i]._getOriginalPositionIn(_rawArtists)
|
||||||
|
@ -398,7 +401,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
}
|
}
|
||||||
|
|
||||||
check(_genres.isNotEmpty()) { "Malformed song: No genres" }
|
check(_genres.isNotEmpty()) { "Malformed song: No genres" }
|
||||||
for( i in _genres.indices ) {
|
for (i in _genres.indices) {
|
||||||
// Non-destructively reorder the linked genres so that they align with
|
// Non-destructively reorder the linked genres so that they align with
|
||||||
// the genre ordering within the song metadata.
|
// the genre ordering within the song metadata.
|
||||||
val newIdx = _genres[i]._getOriginalPositionIn(_rawGenres)
|
val newIdx = _genres[i]._getOriginalPositionIn(_rawGenres)
|
||||||
|
@ -445,14 +448,15 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent() {
|
class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent() {
|
||||||
override val uid = raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ALBUMS, it) }
|
override val uid =
|
||||||
?: UID.auxio(MusicMode.ALBUMS) {
|
raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ALBUMS, it) }
|
||||||
// Hash based on only names despite the presence of a date to increase stability.
|
?: UID.auxio(MusicMode.ALBUMS) {
|
||||||
// I don't know if there is any situation where an artist will have two albums with
|
// Hash based on only names despite the presence of a date to increase stability.
|
||||||
// the exact same name, but if there is, I would love to know.
|
// I don't know if there is any situation where an artist will have two albums with
|
||||||
update(raw.name)
|
// the exact same name, but if there is, I would love to know.
|
||||||
update(raw.rawArtists.map { it.name })
|
update(raw.name)
|
||||||
}
|
update(raw.rawArtists.map { it.name })
|
||||||
|
}
|
||||||
|
|
||||||
override val rawName = raw.name
|
override val rawName = raw.name
|
||||||
|
|
||||||
|
@ -481,21 +485,19 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
||||||
val dateAdded: Long
|
val dateAdded: Long
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The artists of this album. Usually one, but there may be more. These are derived from
|
* The artists of this album. Usually one, but there may be more. These are derived from the
|
||||||
* the album artist first, so they may differ from the song artists.
|
* album artist first, so they may differ from the song artists.
|
||||||
*/
|
*/
|
||||||
private val _artists = mutableListOf<Artist>()
|
private val _artists = mutableListOf<Artist>()
|
||||||
val artists: List<Artist> get() = _artists
|
val artists: List<Artist>
|
||||||
|
get() = _artists
|
||||||
|
|
||||||
|
/** Resolve the artists of this album in a human-readable manner. */
|
||||||
|
fun resolveArtistContents(context: Context) = artists.joinToString { it.resolveName(context) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the artists of this album in a human-readable manner.
|
* Utility for RecyclerView differs to check if resolveArtistContents is the same without a
|
||||||
*/
|
* context.
|
||||||
fun resolveArtistContents(context: Context) =
|
|
||||||
artists.joinToString { it.resolveName(context) }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility for RecyclerView differs to check if resolveArtistContents is the same without
|
|
||||||
* a context.
|
|
||||||
*/
|
*/
|
||||||
fun areArtistContentsTheSame(other: Album): Boolean {
|
fun areArtistContentsTheSame(other: Album): Boolean {
|
||||||
for (i in 0 until max(artists.size, other.artists.size)) {
|
for (i in 0 until max(artists.size, other.artists.size)) {
|
||||||
|
@ -547,7 +549,7 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
||||||
override fun _finalize() {
|
override fun _finalize() {
|
||||||
check(songs.isNotEmpty()) { "Malformed album: Empty" }
|
check(songs.isNotEmpty()) { "Malformed album: Empty" }
|
||||||
check(_artists.isNotEmpty()) { "Malformed album: No artists" }
|
check(_artists.isNotEmpty()) { "Malformed album: No artists" }
|
||||||
for( i in _artists.indices ) {
|
for (i in _artists.indices) {
|
||||||
// Non-destructively reorder the linked artists so that they align with
|
// Non-destructively reorder the linked artists so that they align with
|
||||||
// the artist ordering within the song metadata.
|
// the artist ordering within the song metadata.
|
||||||
val newIdx = _artists[i]._getOriginalPositionIn(_rawArtists)
|
val newIdx = _artists[i]._getOriginalPositionIn(_rawArtists)
|
||||||
|
@ -572,9 +574,9 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (other !is Raw) return false
|
if (other !is Raw) return false
|
||||||
if (musicBrainzId != null && other.musicBrainzId != null &&
|
if (musicBrainzId != null &&
|
||||||
musicBrainzId == other.musicBrainzId
|
other.musicBrainzId != null &&
|
||||||
) {
|
musicBrainzId == other.musicBrainzId) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -584,15 +586,14 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract artist. This is derived from both album artist values and artist values in
|
* An abstract artist. This is derived from both album artist values and artist values in albums and
|
||||||
* albums and songs respectively.
|
* songs respectively.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class Artist
|
class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
|
||||||
constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
|
override val uid =
|
||||||
override val uid = raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ARTISTS, it) } ?: UID.auxio(
|
raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ARTISTS, it) }
|
||||||
MusicMode.ARTISTS
|
?: UID.auxio(MusicMode.ARTISTS) { update(raw.name) }
|
||||||
) { update(raw.name) }
|
|
||||||
|
|
||||||
override val rawName = raw.name
|
override val rawName = raw.name
|
||||||
|
|
||||||
|
@ -602,9 +603,7 @@ constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
|
||||||
|
|
||||||
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_artist)
|
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_artist)
|
||||||
|
|
||||||
/**
|
/** The songs of this artist. This might be empty. */
|
||||||
* The songs of this artist. This might be empty.
|
|
||||||
*/
|
|
||||||
override val songs: List<Song>
|
override val songs: List<Song>
|
||||||
|
|
||||||
/** The total duration of songs in this artist, in millis. Null if there are no songs. */
|
/** The total duration of songs in this artist, in millis. Null if there are no songs. */
|
||||||
|
@ -618,14 +617,12 @@ constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
|
||||||
|
|
||||||
private lateinit var genres: List<Genre>
|
private lateinit var genres: List<Genre>
|
||||||
|
|
||||||
/**
|
/** Resolve the combined genres of this artist into a human-readable string. */
|
||||||
* Resolve the combined genres of this artist into a human-readable string.
|
|
||||||
*/
|
|
||||||
fun resolveGenreContents(context: Context) = genres.joinToString { it.resolveName(context) }
|
fun resolveGenreContents(context: Context) = genres.joinToString { it.resolveName(context) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility for RecyclerView differs to check if resolveGenreContents is the same without
|
* Utility for RecyclerView differs to check if resolveGenreContents is the same without a
|
||||||
* a context.
|
* context.
|
||||||
*/
|
*/
|
||||||
fun areGenreContentsTheSame(other: Artist): Boolean {
|
fun areGenreContentsTheSame(other: Artist): Boolean {
|
||||||
for (i in 0 until max(genres.size, other.genres.size)) {
|
for (i in 0 until max(genres.size, other.genres.size)) {
|
||||||
|
@ -639,11 +636,10 @@ constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val distinctSongs = mutableSetOf<Song>()
|
val distinctSongs = mutableSetOf<Song>()
|
||||||
val distinctAlbums = mutableSetOf<Album>()
|
val distinctAlbums = mutableSetOf<Album>()
|
||||||
|
|
||||||
var noAlbums = true
|
var noAlbums = true
|
||||||
|
|
||||||
for (music in songAlbums) {
|
for (music in songAlbums) {
|
||||||
|
@ -653,13 +649,11 @@ constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
|
||||||
distinctSongs.add(music)
|
distinctSongs.add(music)
|
||||||
distinctAlbums.add(music.album)
|
distinctAlbums.add(music.album)
|
||||||
}
|
}
|
||||||
|
|
||||||
is Album -> {
|
is Album -> {
|
||||||
music._link(this)
|
music._link(this)
|
||||||
distinctAlbums.add(music)
|
distinctAlbums.add(music)
|
||||||
noAlbums = false
|
noAlbums = false
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> error("Unexpected input music ${music::class.simpleName}")
|
else -> error("Unexpected input music ${music::class.simpleName}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -677,11 +671,17 @@ constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
|
||||||
override fun _finalize() {
|
override fun _finalize() {
|
||||||
check(songs.isNotEmpty() || albums.isNotEmpty()) { "Malformed artist: Empty" }
|
check(songs.isNotEmpty() || albums.isNotEmpty()) { "Malformed artist: Empty" }
|
||||||
|
|
||||||
genres = Sort(Sort.Mode.ByName, true).genres(songs.flatMapTo(mutableSetOf()) { it.genres })
|
genres =
|
||||||
.sortedByDescending { genre -> songs.count { it.genres.contains(genre) } }
|
Sort(Sort.Mode.ByName, true)
|
||||||
|
.genres(songs.flatMapTo(mutableSetOf()) { it.genres })
|
||||||
|
.sortedByDescending { genre -> songs.count { it.genres.contains(genre) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
class Raw(val musicBrainzId: UUID? = null, val name: String? = null, val sortName: String? = null) {
|
class Raw(
|
||||||
|
val musicBrainzId: UUID? = null,
|
||||||
|
val name: String? = null,
|
||||||
|
val sortName: String? = null
|
||||||
|
) {
|
||||||
private val hashCode = musicBrainzId?.hashCode() ?: name?.lowercase().hashCode()
|
private val hashCode = musicBrainzId?.hashCode() ?: name?.lowercase().hashCode()
|
||||||
|
|
||||||
override fun hashCode() = hashCode
|
override fun hashCode() = hashCode
|
||||||
|
@ -689,9 +689,9 @@ constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (other !is Raw) return false
|
if (other !is Raw) return false
|
||||||
|
|
||||||
if (musicBrainzId != null && other.musicBrainzId != null &&
|
if (musicBrainzId != null &&
|
||||||
musicBrainzId == other.musicBrainzId
|
other.musicBrainzId != null &&
|
||||||
) {
|
musicBrainzId == other.musicBrainzId) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -743,8 +743,8 @@ class Genre constructor(private val raw: Raw, override val songs: List<Song>) :
|
||||||
|
|
||||||
durationMs = totalDuration
|
durationMs = totalDuration
|
||||||
|
|
||||||
albums = Sort(Sort.Mode.ByName, true).albums(distinctAlbums)
|
albums =
|
||||||
.sortedByDescending { album ->
|
Sort(Sort.Mode.ByName, true).albums(distinctAlbums).sortedByDescending { album ->
|
||||||
album.songs.count { it.genres.contains(this) }
|
album.songs.count { it.genres.contains(this) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -819,9 +819,7 @@ fun MessageDigest.update(n: Long?) {
|
||||||
n.shr(32).toByte(),
|
n.shr(32).toByte(),
|
||||||
n.shr(40).toByte(),
|
n.shr(40).toByte(),
|
||||||
n.shl(48).toByte(),
|
n.shl(48).toByte(),
|
||||||
n.shr(56).toByte()
|
n.shr(56).toByte()))
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -851,6 +849,5 @@ fun ByteArray.toUuid(): UUID {
|
||||||
.or(get(12).toLong().and(0xFF).shl(24))
|
.or(get(12).toLong().and(0xFF).shl(24))
|
||||||
.or(get(13).toLong().and(0xFF).shl(16))
|
.or(get(13).toLong().and(0xFF).shl(16))
|
||||||
.or(get(14).toLong().and(0xFF).shl(8))
|
.or(get(14).toLong().and(0xFF).shl(8))
|
||||||
.or(get(15).toLong().and(0xFF))
|
.or(get(15).toLong().and(0xFF)))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,11 +99,10 @@ class MusicStore private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a music [T] by its [uid]. If the music does not exist, or if the music is
|
* Find a music [T] by its [uid]. If the music does not exist, or if the music is not [T],
|
||||||
* not [T], null will be returned.
|
* null will be returned.
|
||||||
*/
|
*/
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST") fun <T : Music> find(uid: Music.UID) = uidMap[uid] as? T
|
||||||
fun <T : Music> find(uid: Music.UID) = uidMap[uid] as? T
|
|
||||||
|
|
||||||
/** Sanitize an old item to find the corresponding item in a new library. */
|
/** Sanitize an old item to find the corresponding item in a new library. */
|
||||||
fun sanitize(song: Song) = find<Song>(song.uid)
|
fun sanitize(song: Song) = find<Song>(song.uid)
|
||||||
|
@ -122,8 +121,8 @@ class MusicStore private constructor() {
|
||||||
|
|
||||||
/** Find a song for a [uri]. */
|
/** Find a song for a [uri]. */
|
||||||
fun findSongForUri(context: Context, uri: Uri) =
|
fun findSongForUri(context: Context, uri: Uri) =
|
||||||
context.contentResolverSafe.useQuery(uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) {
|
context.contentResolverSafe.useQuery(
|
||||||
cursor ->
|
uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) { cursor ->
|
||||||
cursor.moveToFirst()
|
cursor.moveToFirst()
|
||||||
|
|
||||||
// We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a
|
// We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a
|
||||||
|
@ -132,9 +131,7 @@ class MusicStore private constructor() {
|
||||||
val displayName =
|
val displayName =
|
||||||
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
|
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
|
||||||
|
|
||||||
val size = cursor.getLong(
|
val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
|
||||||
cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)
|
|
||||||
)
|
|
||||||
|
|
||||||
songs.find { it.path.name == displayName && it.size == size }
|
songs.find { it.path.name == displayName && it.size == size }
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,9 +44,7 @@ class MusicViewModel : ViewModel(), Indexer.Callback {
|
||||||
indexer.registerCallback(this)
|
indexer.registerCallback(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Re-index the music library. */
|
||||||
* Re-index the music library.
|
|
||||||
*/
|
|
||||||
fun reindex() {
|
fun reindex() {
|
||||||
indexer.requestReindex(true)
|
indexer.requestReindex(true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,10 +18,10 @@
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
|
import kotlin.math.max
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.Sort.Mode
|
import org.oxycblt.auxio.music.Sort.Mode
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the sort modes used in Auxio.
|
* Represents the sort modes used in Auxio.
|
||||||
|
@ -79,11 +79,11 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
albums.sortWith(mode.getAlbumComparator(isAscending))
|
albums.sortWith(mode.getAlbumComparator(isAscending))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun artistsInPlace(artists: MutableList<Artist>) {
|
private fun artistsInPlace(artists: MutableList<Artist>) {
|
||||||
artists.sortWith(mode.getArtistComparator(isAscending))
|
artists.sortWith(mode.getArtistComparator(isAscending))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun genresInPlace(genres: MutableList<Genre>) {
|
private fun genresInPlace(genres: MutableList<Genre>) {
|
||||||
genres.sortWith(mode.getGenreComparator(isAscending))
|
genres.sortWith(mode.getGenreComparator(isAscending))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,8 +141,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
compareByDynamic(ascending, BasicComparator.ALBUM) { it.album },
|
compareByDynamic(ascending, BasicComparator.ALBUM) { it.album },
|
||||||
compareBy(NullableComparator.INT) { it.disc },
|
compareBy(NullableComparator.INT) { it.disc },
|
||||||
compareBy(NullableComparator.INT) { it.track },
|
compareBy(NullableComparator.INT) { it.track },
|
||||||
compareBy(BasicComparator.SONG)
|
compareBy(BasicComparator.SONG))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sort by the artist of an item, only supported by [Album] and [Song] */
|
/** Sort by the artist of an item, only supported by [Album] and [Song] */
|
||||||
|
@ -160,15 +159,13 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
compareByDescending(BasicComparator.ALBUM) { it.album },
|
compareByDescending(BasicComparator.ALBUM) { it.album },
|
||||||
compareBy(NullableComparator.INT) { it.disc },
|
compareBy(NullableComparator.INT) { it.disc },
|
||||||
compareBy(NullableComparator.INT) { it.track },
|
compareBy(NullableComparator.INT) { it.track },
|
||||||
compareBy(BasicComparator.SONG)
|
compareBy(BasicComparator.SONG))
|
||||||
)
|
|
||||||
|
|
||||||
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
|
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(ascending, ListComparator.ARTISTS) { it.artists },
|
compareByDynamic(ascending, ListComparator.ARTISTS) { it.artists },
|
||||||
compareByDescending(NullableComparator.DATE) { it.date },
|
compareByDescending(NullableComparator.DATE) { it.date },
|
||||||
compareBy(BasicComparator.ALBUM)
|
compareBy(BasicComparator.ALBUM))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sort by the date of an item, only supported by [Album] and [Song] */
|
/** Sort by the date of an item, only supported by [Album] and [Song] */
|
||||||
|
@ -185,14 +182,12 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
compareByDescending(BasicComparator.ALBUM) { it.album },
|
compareByDescending(BasicComparator.ALBUM) { it.album },
|
||||||
compareBy(NullableComparator.INT) { it.disc },
|
compareBy(NullableComparator.INT) { it.disc },
|
||||||
compareBy(NullableComparator.INT) { it.track },
|
compareBy(NullableComparator.INT) { it.track },
|
||||||
compareBy(BasicComparator.SONG)
|
compareBy(BasicComparator.SONG))
|
||||||
)
|
|
||||||
|
|
||||||
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
|
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(ascending, NullableComparator.DATE) { it.date },
|
compareByDynamic(ascending, NullableComparator.DATE) { it.date },
|
||||||
compareBy(BasicComparator.ALBUM)
|
compareBy(BasicComparator.ALBUM))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sort by the duration of the item. Supports all items. */
|
/** Sort by the duration of the item. Supports all items. */
|
||||||
|
@ -205,27 +200,20 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
|
|
||||||
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
|
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(ascending) { it.durationMs },
|
compareByDynamic(ascending) { it.durationMs }, compareBy(BasicComparator.SONG))
|
||||||
compareBy(BasicComparator.SONG)
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
|
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(ascending) { it.durationMs },
|
compareByDynamic(ascending) { it.durationMs }, compareBy(BasicComparator.ALBUM))
|
||||||
compareBy(BasicComparator.ALBUM)
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
|
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(ascending, NullableComparator.LONG) { it.durationMs },
|
compareByDynamic(ascending, NullableComparator.LONG) { it.durationMs },
|
||||||
compareBy(BasicComparator.ARTIST)
|
compareBy(BasicComparator.ARTIST))
|
||||||
)
|
|
||||||
|
|
||||||
override fun getGenreComparator(ascending: Boolean): Comparator<Genre> =
|
override fun getGenreComparator(ascending: Boolean): Comparator<Genre> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(ascending) { it.durationMs },
|
compareByDynamic(ascending) { it.durationMs }, compareBy(BasicComparator.GENRE))
|
||||||
compareBy(BasicComparator.GENRE)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sort by the amount of songs. Only applicable to music parents. */
|
/** Sort by the amount of songs. Only applicable to music parents. */
|
||||||
|
@ -238,21 +226,16 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
|
|
||||||
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
|
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(ascending) { it.songs.size },
|
compareByDynamic(ascending) { it.songs.size }, compareBy(BasicComparator.ALBUM))
|
||||||
compareBy(BasicComparator.ALBUM)
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
|
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(ascending, NullableComparator.INT) { it.songs.size },
|
compareByDynamic(ascending, NullableComparator.INT) { it.songs.size },
|
||||||
compareBy(BasicComparator.ARTIST)
|
compareBy(BasicComparator.ARTIST))
|
||||||
)
|
|
||||||
|
|
||||||
override fun getGenreComparator(ascending: Boolean): Comparator<Genre> =
|
override fun getGenreComparator(ascending: Boolean): Comparator<Genre> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(ascending) { it.songs.size },
|
compareByDynamic(ascending) { it.songs.size }, compareBy(BasicComparator.GENRE))
|
||||||
compareBy(BasicComparator.GENRE)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sort by the disc, and then track number of an item. Only supported by [Song]. */
|
/** Sort by the disc, and then track number of an item. Only supported by [Song]. */
|
||||||
|
@ -267,8 +250,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(ascending, NullableComparator.INT) { it.disc },
|
compareByDynamic(ascending, NullableComparator.INT) { it.disc },
|
||||||
compareBy(NullableComparator.INT) { it.track },
|
compareBy(NullableComparator.INT) { it.track },
|
||||||
compareBy(BasicComparator.SONG)
|
compareBy(BasicComparator.SONG))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -286,8 +268,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareBy(NullableComparator.INT) { it.disc },
|
compareBy(NullableComparator.INT) { it.disc },
|
||||||
compareByDynamic(ascending, NullableComparator.INT) { it.track },
|
compareByDynamic(ascending, NullableComparator.INT) { it.track },
|
||||||
compareBy(BasicComparator.SONG)
|
compareBy(BasicComparator.SONG))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sort by the time the item was added. Only supported by [Song] */
|
/** Sort by the time the item was added. Only supported by [Song] */
|
||||||
|
@ -300,15 +281,12 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
|
|
||||||
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
|
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(ascending) { it.dateAdded },
|
compareByDynamic(ascending) { it.dateAdded }, compareBy(BasicComparator.SONG))
|
||||||
compareBy(BasicComparator.SONG)
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
|
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(ascending) { album -> album.songs.minOf { it.dateAdded } },
|
compareByDynamic(ascending) { album -> album.songs.minOf { it.dateAdded } },
|
||||||
compareBy(BasicComparator.ALBUM)
|
compareBy(BasicComparator.ALBUM))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected inline fun <T : Music, K> compareByDynamic(
|
protected inline fun <T : Music, K> compareByDynamic(
|
||||||
|
|
|
@ -19,22 +19,18 @@ package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.format.DateUtils
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import org.oxycblt.auxio.BuildConfig
|
|
||||||
import org.oxycblt.auxio.R
|
|
||||||
import org.oxycblt.auxio.playback.secsToMs
|
|
||||||
import org.oxycblt.auxio.util.inRangeOrNull
|
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
import org.oxycblt.auxio.util.logE
|
|
||||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.time.temporal.TemporalQueries
|
import java.time.temporal.TemporalQueries
|
||||||
import java.util.Formatter
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
import org.oxycblt.auxio.BuildConfig
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.util.inRangeOrNull
|
||||||
|
import org.oxycblt.auxio.util.logE
|
||||||
|
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An ISO-8601/RFC 3339 Date.
|
* An ISO-8601/RFC 3339 Date.
|
||||||
|
@ -81,9 +77,8 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
|
||||||
private val second = tokens.getOrNull(5)
|
private val second = tokens.getOrNull(5)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve this date into a string. This could result in a year string formatted
|
* Resolve this date into a string. This could result in a year string formatted as "YYYY", or a
|
||||||
* as "YYYY", or a month and year string formatted as "MMM YYYY" depending on the
|
* month and year string formatted as "MMM YYYY" depending on the situation.
|
||||||
* situation.
|
|
||||||
*/
|
*/
|
||||||
fun resolveDate(context: Context): String {
|
fun resolveDate(context: Context): String {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
@ -101,17 +96,17 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
private fun resolveFullDate(context: Context) =
|
private fun resolveFullDate(context: Context) =
|
||||||
if( month != null ) {
|
if (month != null) {
|
||||||
val temporal = DateTimeFormatter.ISO_DATE.parse(
|
val temporal =
|
||||||
"$year-$month-${day ?: 1}",
|
DateTimeFormatter.ISO_DATE.parse(
|
||||||
TemporalQueries.localDate()
|
"$year-$month-${day ?: 1}", TemporalQueries.localDate())
|
||||||
)
|
|
||||||
|
|
||||||
// When it comes to songs, we only want to show the month and year. This
|
// When it comes to songs, we only want to show the month and year. This
|
||||||
// cannot be done with DateUtils due to it's dynamic nature, so instead
|
// cannot be done with DateUtils due to it's dynamic nature, so instead
|
||||||
// it's done with the built-in date formatter. Since the legacy date API
|
// it's done with the built-in date formatter. Since the legacy date API
|
||||||
// is awful, we only use instant and limit it to Android 8 onwards.
|
// is awful, we only use instant and limit it to Android 8 onwards.
|
||||||
temporal.atStartOfDay(ZoneId.systemDefault())
|
temporal
|
||||||
|
.atStartOfDay(ZoneId.systemDefault())
|
||||||
.format(DateTimeFormatter.ofPattern("MMM yyyy", Locale.getDefault()))
|
.format(DateTimeFormatter.ofPattern("MMM yyyy", Locale.getDefault()))
|
||||||
} else {
|
} else {
|
||||||
resolveYear(context)
|
resolveYear(context)
|
||||||
|
@ -161,8 +156,7 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
|
||||||
companion object {
|
companion object {
|
||||||
private val ISO8601_REGEX =
|
private val ISO8601_REGEX =
|
||||||
Regex(
|
Regex(
|
||||||
"""^(\d{4,})([-.](\d{2})([-.](\d{2})([T ](\d{2})([:.](\d{2})([:.](\d{2})(Z)?)?)?)?)?)?$"""
|
"""^(\d{4,})([-.](\d{2})([-.](\d{2})([T ](\d{2})([:.](\d{2})([:.](\d{2})(Z)?)?)?)?)?)?$""")
|
||||||
)
|
|
||||||
|
|
||||||
fun from(year: Int) = fromTokens(listOf(year))
|
fun from(year: Int) = fromTokens(listOf(year))
|
||||||
|
|
||||||
|
@ -246,11 +240,12 @@ sealed class ReleaseType {
|
||||||
|
|
||||||
data class Compilation(override val refinement: Refinement?) : ReleaseType() {
|
data class Compilation(override val refinement: Refinement?) : ReleaseType() {
|
||||||
override val stringRes: Int
|
override val stringRes: Int
|
||||||
get() = when (refinement) {
|
get() =
|
||||||
null -> R.string.lbl_compilation
|
when (refinement) {
|
||||||
Refinement.LIVE -> R.string.lbl_compilation_live
|
null -> R.string.lbl_compilation
|
||||||
Refinement.REMIX -> R.string.lbl_compilation_remix
|
Refinement.LIVE -> R.string.lbl_compilation_live
|
||||||
}
|
Refinement.REMIX -> R.string.lbl_compilation_remix
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object Soundtrack : ReleaseType() {
|
object Soundtrack : ReleaseType() {
|
||||||
|
@ -325,14 +320,15 @@ sealed class ReleaseType {
|
||||||
private inline fun parseSecondaryTypeImpl(
|
private inline fun parseSecondaryTypeImpl(
|
||||||
type: String?,
|
type: String?,
|
||||||
convertRefinement: (Refinement?) -> ReleaseType
|
convertRefinement: (Refinement?) -> ReleaseType
|
||||||
) = when {
|
) =
|
||||||
// Parse all the types that have no children
|
when {
|
||||||
type.equals("soundtrack", true) -> Soundtrack
|
// Parse all the types that have no children
|
||||||
type.equals("mixtape/street", true) -> Mixtape
|
type.equals("soundtrack", true) -> Soundtrack
|
||||||
type.equals("dj-mix", true) -> Mix
|
type.equals("mixtape/street", true) -> Mixtape
|
||||||
type.equals("live", true) -> convertRefinement(Refinement.LIVE)
|
type.equals("dj-mix", true) -> Mix
|
||||||
type.equals("remix", true) -> convertRefinement(Refinement.REMIX)
|
type.equals("live", true) -> convertRefinement(Refinement.LIVE)
|
||||||
else -> convertRefinement(null)
|
type.equals("remix", true) -> convertRefinement(Refinement.REMIX)
|
||||||
}
|
else -> convertRefinement(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,18 +24,18 @@ import android.database.sqlite.SQLiteOpenHelper
|
||||||
import androidx.core.database.getIntOrNull
|
import androidx.core.database.getIntOrNull
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
import androidx.core.database.sqlite.transaction
|
import androidx.core.database.sqlite.transaction
|
||||||
|
import java.io.File
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
import org.oxycblt.auxio.util.queryAll
|
import org.oxycblt.auxio.util.queryAll
|
||||||
import org.oxycblt.auxio.util.requireBackgroundThread
|
import org.oxycblt.auxio.util.requireBackgroundThread
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The extractor that caches music metadata for faster use later. The cache is only responsible for
|
* The extractor that caches music metadata for faster use later. The cache is only responsible for
|
||||||
* storing "intrinsic" data, as in information derived from the file format and not
|
* storing "intrinsic" data, as in information derived from the file format and not information from
|
||||||
* information from the media database or file system. The exceptions are the database ID and
|
* the media database or file system. The exceptions are the database ID and modification times for
|
||||||
* modification times for files, as these are required for the cache to function well.
|
* files, as these are required for the cache to function well.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class CacheExtractor(private val context: Context, private val noop: Boolean) {
|
class CacheExtractor(private val context: Context, private val noop: Boolean) {
|
||||||
|
@ -55,9 +55,7 @@ class CacheExtractor(private val context: Context, private val noop: Boolean) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Write a list of newly-indexed raw songs to the database. */
|
||||||
* Write a list of newly-indexed raw songs to the database.
|
|
||||||
*/
|
|
||||||
fun finalize(rawSongs: List<Song.Raw>) {
|
fun finalize(rawSongs: List<Song.Raw>) {
|
||||||
cacheMap = null
|
cacheMap = null
|
||||||
|
|
||||||
|
@ -75,14 +73,16 @@ class CacheExtractor(private val context: Context, private val noop: Boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maybe copy a cached raw song into this instance, assuming that it has not changed
|
* Maybe copy a cached raw song into this instance, assuming that it has not changed since it
|
||||||
* since it was last saved. Returns true if a song was loaded.
|
* was last saved. Returns true if a song was loaded.
|
||||||
*/
|
*/
|
||||||
fun populateFromCache(rawSong: Song.Raw): Boolean {
|
fun populateFromCache(rawSong: Song.Raw): Boolean {
|
||||||
val map = cacheMap ?: return false
|
val map = cacheMap ?: return false
|
||||||
|
|
||||||
val cachedRawSong = map[rawSong.mediaStoreId]
|
val cachedRawSong = map[rawSong.mediaStoreId]
|
||||||
if (cachedRawSong != null && cachedRawSong.dateAdded == rawSong.dateAdded && cachedRawSong.dateModified == rawSong.dateModified) {
|
if (cachedRawSong != null &&
|
||||||
|
cachedRawSong.dateAdded == rawSong.dateAdded &&
|
||||||
|
cachedRawSong.dateModified == rawSong.dateModified) {
|
||||||
rawSong.musicBrainzId = cachedRawSong.musicBrainzId
|
rawSong.musicBrainzId = cachedRawSong.musicBrainzId
|
||||||
rawSong.name = cachedRawSong.name
|
rawSong.name = cachedRawSong.name
|
||||||
rawSong.sortName = cachedRawSong.sortName
|
rawSong.sortName = cachedRawSong.sortName
|
||||||
|
@ -118,33 +118,35 @@ class CacheExtractor(private val context: Context, private val noop: Boolean) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CacheDatabase(context: Context) : SQLiteOpenHelper(context, File(context.cacheDir, DB_NAME).absolutePath, null, DB_VERSION) {
|
private class CacheDatabase(context: Context) :
|
||||||
|
SQLiteOpenHelper(context, File(context.cacheDir, DB_NAME).absolutePath, null, DB_VERSION) {
|
||||||
override fun onCreate(db: SQLiteDatabase) {
|
override fun onCreate(db: SQLiteDatabase) {
|
||||||
val command = StringBuilder()
|
val command =
|
||||||
.append("CREATE TABLE IF NOT EXISTS $TABLE_RAW_SONGS(")
|
StringBuilder()
|
||||||
.append("${Columns.MEDIA_STORE_ID} LONG PRIMARY KEY,")
|
.append("CREATE TABLE IF NOT EXISTS $TABLE_RAW_SONGS(")
|
||||||
.append("${Columns.DATE_ADDED} LONG NOT NULL,")
|
.append("${Columns.MEDIA_STORE_ID} LONG PRIMARY KEY,")
|
||||||
.append("${Columns.DATE_MODIFIED} LONG NOT NULL,")
|
.append("${Columns.DATE_ADDED} LONG NOT NULL,")
|
||||||
.append("${Columns.SIZE} LONG NOT NULL,")
|
.append("${Columns.DATE_MODIFIED} LONG NOT NULL,")
|
||||||
.append("${Columns.DURATION} LONG NOT NULL,")
|
.append("${Columns.SIZE} LONG NOT NULL,")
|
||||||
.append("${Columns.FORMAT_MIME_TYPE} STRING,")
|
.append("${Columns.DURATION} LONG NOT NULL,")
|
||||||
.append("${Columns.MUSIC_BRAINZ_ID} STRING,")
|
.append("${Columns.FORMAT_MIME_TYPE} STRING,")
|
||||||
.append("${Columns.NAME} STRING NOT NULL,")
|
.append("${Columns.MUSIC_BRAINZ_ID} STRING,")
|
||||||
.append("${Columns.SORT_NAME} STRING,")
|
.append("${Columns.NAME} STRING NOT NULL,")
|
||||||
.append("${Columns.TRACK} INT,")
|
.append("${Columns.SORT_NAME} STRING,")
|
||||||
.append("${Columns.DISC} INT,")
|
.append("${Columns.TRACK} INT,")
|
||||||
.append("${Columns.DATE} STRING,")
|
.append("${Columns.DISC} INT,")
|
||||||
.append("${Columns.ALBUM_MUSIC_BRAINZ_ID} STRING,")
|
.append("${Columns.DATE} STRING,")
|
||||||
.append("${Columns.ALBUM_NAME} STRING NOT NULL,")
|
.append("${Columns.ALBUM_MUSIC_BRAINZ_ID} STRING,")
|
||||||
.append("${Columns.ALBUM_SORT_NAME} STRING,")
|
.append("${Columns.ALBUM_NAME} STRING NOT NULL,")
|
||||||
.append("${Columns.ALBUM_RELEASE_TYPES} STRING,")
|
.append("${Columns.ALBUM_SORT_NAME} STRING,")
|
||||||
.append("${Columns.ARTIST_MUSIC_BRAINZ_IDS} STRING,")
|
.append("${Columns.ALBUM_RELEASE_TYPES} STRING,")
|
||||||
.append("${Columns.ARTIST_NAMES} STRING,")
|
.append("${Columns.ARTIST_MUSIC_BRAINZ_IDS} STRING,")
|
||||||
.append("${Columns.ARTIST_SORT_NAMES} STRING,")
|
.append("${Columns.ARTIST_NAMES} STRING,")
|
||||||
.append("${Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS} STRING,")
|
.append("${Columns.ARTIST_SORT_NAMES} STRING,")
|
||||||
.append("${Columns.ALBUM_ARTIST_NAMES} STRING,")
|
.append("${Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS} STRING,")
|
||||||
.append("${Columns.ALBUM_ARTIST_SORT_NAMES} STRING,")
|
.append("${Columns.ALBUM_ARTIST_NAMES} STRING,")
|
||||||
.append("${Columns.GENRE_NAMES} STRING)")
|
.append("${Columns.ALBUM_ARTIST_SORT_NAMES} STRING,")
|
||||||
|
.append("${Columns.GENRE_NAMES} STRING)")
|
||||||
|
|
||||||
db.execSQL(command.toString())
|
db.execSQL(command.toString())
|
||||||
}
|
}
|
||||||
|
@ -185,18 +187,22 @@ private class CacheDatabase(context: Context) : SQLiteOpenHelper(context, File(c
|
||||||
val discIndex = cursor.getColumnIndexOrThrow(Columns.DISC)
|
val discIndex = cursor.getColumnIndexOrThrow(Columns.DISC)
|
||||||
val dateIndex = cursor.getColumnIndexOrThrow(Columns.DATE)
|
val dateIndex = cursor.getColumnIndexOrThrow(Columns.DATE)
|
||||||
|
|
||||||
val albumMusicBrainzIdIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_MUSIC_BRAINZ_ID)
|
val albumMusicBrainzIdIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(Columns.ALBUM_MUSIC_BRAINZ_ID)
|
||||||
val albumNameIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_NAME)
|
val albumNameIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_NAME)
|
||||||
val albumSortNameIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_SORT_NAME)
|
val albumSortNameIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_SORT_NAME)
|
||||||
val albumReleaseTypesIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_RELEASE_TYPES)
|
val albumReleaseTypesIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_RELEASE_TYPES)
|
||||||
|
|
||||||
val artistMusicBrainzIdsIndex = cursor.getColumnIndexOrThrow(Columns.ARTIST_MUSIC_BRAINZ_IDS)
|
val artistMusicBrainzIdsIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(Columns.ARTIST_MUSIC_BRAINZ_IDS)
|
||||||
val artistNamesIndex = cursor.getColumnIndexOrThrow(Columns.ARTIST_NAMES)
|
val artistNamesIndex = cursor.getColumnIndexOrThrow(Columns.ARTIST_NAMES)
|
||||||
val artistSortNamesIndex = cursor.getColumnIndexOrThrow(Columns.ARTIST_SORT_NAMES)
|
val artistSortNamesIndex = cursor.getColumnIndexOrThrow(Columns.ARTIST_SORT_NAMES)
|
||||||
|
|
||||||
val albumArtistMusicBrainzIdsIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS)
|
val albumArtistMusicBrainzIdsIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS)
|
||||||
val albumArtistNamesIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_NAMES)
|
val albumArtistNamesIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_NAMES)
|
||||||
val albumArtistSortNamesIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_SORT_NAMES)
|
val albumArtistSortNamesIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_SORT_NAMES)
|
||||||
|
|
||||||
val genresIndex = cursor.getColumnIndexOrThrow(Columns.GENRE_NAMES)
|
val genresIndex = cursor.getColumnIndexOrThrow(Columns.GENRE_NAMES)
|
||||||
|
|
||||||
|
@ -223,26 +229,31 @@ private class CacheDatabase(context: Context) : SQLiteOpenHelper(context, File(c
|
||||||
raw.albumMusicBrainzId = cursor.getStringOrNull(albumMusicBrainzIdIndex)
|
raw.albumMusicBrainzId = cursor.getStringOrNull(albumMusicBrainzIdIndex)
|
||||||
raw.albumName = cursor.getString(albumNameIndex)
|
raw.albumName = cursor.getString(albumNameIndex)
|
||||||
raw.albumSortName = cursor.getStringOrNull(albumSortNameIndex)
|
raw.albumSortName = cursor.getStringOrNull(albumSortNameIndex)
|
||||||
cursor.getStringOrNull(albumReleaseTypesIndex)?.parseMultiValue()
|
cursor.getStringOrNull(albumReleaseTypesIndex)?.parseMultiValue()?.let {
|
||||||
?.let { raw.albumReleaseTypes = it }
|
raw.albumReleaseTypes = it
|
||||||
|
}
|
||||||
|
|
||||||
cursor.getStringOrNull(artistMusicBrainzIdsIndex)?.let {
|
cursor.getStringOrNull(artistMusicBrainzIdsIndex)?.let {
|
||||||
raw.artistMusicBrainzIds = it.parseMultiValue()
|
raw.artistMusicBrainzIds = it.parseMultiValue()
|
||||||
}
|
}
|
||||||
cursor.getStringOrNull(artistNamesIndex)
|
cursor.getStringOrNull(artistNamesIndex)?.let {
|
||||||
?.let { raw.artistNames = it.parseMultiValue() }
|
raw.artistNames = it.parseMultiValue()
|
||||||
cursor.getStringOrNull(artistSortNamesIndex)
|
}
|
||||||
?.let { raw.artistSortNames = it.parseMultiValue() }
|
cursor.getStringOrNull(artistSortNamesIndex)?.let {
|
||||||
|
raw.artistSortNames = it.parseMultiValue()
|
||||||
|
}
|
||||||
|
|
||||||
cursor.getStringOrNull(albumArtistMusicBrainzIdsIndex)
|
cursor.getStringOrNull(albumArtistMusicBrainzIdsIndex)?.let {
|
||||||
?.let { raw.albumArtistMusicBrainzIds = it.parseMultiValue() }
|
raw.albumArtistMusicBrainzIds = it.parseMultiValue()
|
||||||
cursor.getStringOrNull(albumArtistNamesIndex)
|
}
|
||||||
?.let { raw.albumArtistNames = it.parseMultiValue() }
|
cursor.getStringOrNull(albumArtistNamesIndex)?.let {
|
||||||
cursor.getStringOrNull(albumArtistSortNamesIndex)
|
raw.albumArtistNames = it.parseMultiValue()
|
||||||
?.let { raw.albumArtistSortNames = it.parseMultiValue() }
|
}
|
||||||
|
cursor.getStringOrNull(albumArtistSortNamesIndex)?.let {
|
||||||
|
raw.albumArtistSortNames = it.parseMultiValue()
|
||||||
|
}
|
||||||
|
|
||||||
cursor.getStringOrNull(genresIndex)
|
cursor.getStringOrNull(genresIndex)?.let { raw.genreNames = it.parseMultiValue() }
|
||||||
?.let { raw.genreNames = it.parseMultiValue() }
|
|
||||||
|
|
||||||
map[id] = raw
|
map[id] = raw
|
||||||
}
|
}
|
||||||
|
@ -287,15 +298,23 @@ private class CacheDatabase(context: Context) : SQLiteOpenHelper(context, File(c
|
||||||
put(Columns.ALBUM_MUSIC_BRAINZ_ID, rawSong.albumMusicBrainzId)
|
put(Columns.ALBUM_MUSIC_BRAINZ_ID, rawSong.albumMusicBrainzId)
|
||||||
put(Columns.ALBUM_NAME, rawSong.albumName)
|
put(Columns.ALBUM_NAME, rawSong.albumName)
|
||||||
put(Columns.ALBUM_SORT_NAME, rawSong.albumSortName)
|
put(Columns.ALBUM_SORT_NAME, rawSong.albumSortName)
|
||||||
put(Columns.ALBUM_RELEASE_TYPES, rawSong.albumReleaseTypes.toMultiValue())
|
put(
|
||||||
|
Columns.ALBUM_RELEASE_TYPES,
|
||||||
|
rawSong.albumReleaseTypes.toMultiValue())
|
||||||
|
|
||||||
put(Columns.ARTIST_MUSIC_BRAINZ_IDS, rawSong.artistMusicBrainzIds.toMultiValue())
|
put(
|
||||||
|
Columns.ARTIST_MUSIC_BRAINZ_IDS,
|
||||||
|
rawSong.artistMusicBrainzIds.toMultiValue())
|
||||||
put(Columns.ARTIST_NAMES, rawSong.artistNames.toMultiValue())
|
put(Columns.ARTIST_NAMES, rawSong.artistNames.toMultiValue())
|
||||||
put(Columns.ARTIST_SORT_NAMES, rawSong.artistSortNames.toMultiValue())
|
put(Columns.ARTIST_SORT_NAMES, rawSong.artistSortNames.toMultiValue())
|
||||||
|
|
||||||
put(Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS, rawSong.albumArtistMusicBrainzIds.toMultiValue())
|
put(
|
||||||
|
Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS,
|
||||||
|
rawSong.albumArtistMusicBrainzIds.toMultiValue())
|
||||||
put(Columns.ALBUM_ARTIST_NAMES, rawSong.albumArtistNames.toMultiValue())
|
put(Columns.ALBUM_ARTIST_NAMES, rawSong.albumArtistNames.toMultiValue())
|
||||||
put(Columns.ALBUM_ARTIST_SORT_NAMES, rawSong.albumArtistSortNames.toMultiValue())
|
put(
|
||||||
|
Columns.ALBUM_ARTIST_SORT_NAMES,
|
||||||
|
rawSong.albumArtistSortNames.toMultiValue())
|
||||||
|
|
||||||
put(Columns.GENRE_NAMES, rawSong.genreNames.toMultiValue())
|
put(Columns.GENRE_NAMES, rawSong.genreNames.toMultiValue())
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import android.provider.MediaStore
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.database.getIntOrNull
|
import androidx.core.database.getIntOrNull
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
|
import java.io.File
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.storage.Directory
|
import org.oxycblt.auxio.music.storage.Directory
|
||||||
import org.oxycblt.auxio.music.storage.directoryCompat
|
import org.oxycblt.auxio.music.storage.directoryCompat
|
||||||
|
@ -37,7 +38,6 @@ import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.contentResolverSafe
|
import org.oxycblt.auxio.util.contentResolverSafe
|
||||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This file acts as the base for most the black magic required to get a remotely sensible music
|
* This file acts as the base for most the black magic required to get a remotely sensible music
|
||||||
|
@ -100,7 +100,10 @@ import java.io.File
|
||||||
* music loading process.
|
* music loading process.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
abstract class MediaStoreExtractor(private val context: Context, private val cacheDatabase: CacheExtractor) {
|
abstract class MediaStoreExtractor(
|
||||||
|
private val context: Context,
|
||||||
|
private val cacheDatabase: CacheExtractor
|
||||||
|
) {
|
||||||
private var cursor: Cursor? = null
|
private var cursor: Cursor? = null
|
||||||
|
|
||||||
private var idIndex = -1
|
private var idIndex = -1
|
||||||
|
@ -173,13 +176,11 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
|
||||||
|
|
||||||
val cursor =
|
val cursor =
|
||||||
requireNotNull(
|
requireNotNull(
|
||||||
context.contentResolverSafe.queryCursor(
|
context.contentResolverSafe.queryCursor(
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||||
projection,
|
projection,
|
||||||
selector,
|
selector,
|
||||||
args.toTypedArray()
|
args.toTypedArray())) { "Content resolver failure: No Cursor returned" }
|
||||||
)
|
|
||||||
) { "Content resolver failure: No Cursor returned" }
|
|
||||||
.also { cursor = it }
|
.also { cursor = it }
|
||||||
|
|
||||||
idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
|
idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
|
||||||
|
@ -207,8 +208,7 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
|
||||||
// obscure formats where genre support is only really covered by this.
|
// obscure formats where genre support is only really covered by this.
|
||||||
context.contentResolverSafe.useQuery(
|
context.contentResolverSafe.useQuery(
|
||||||
MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
|
MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
|
||||||
arrayOf(MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME)
|
arrayOf(MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME)) { genreCursor ->
|
||||||
) { genreCursor ->
|
|
||||||
val idIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID)
|
val idIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID)
|
||||||
val nameIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME)
|
val nameIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME)
|
||||||
|
|
||||||
|
@ -218,8 +218,7 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
|
||||||
|
|
||||||
context.contentResolverSafe.useQuery(
|
context.contentResolverSafe.useQuery(
|
||||||
MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, id),
|
MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, id),
|
||||||
arrayOf(MediaStore.Audio.Genres.Members._ID)
|
arrayOf(MediaStore.Audio.Genres.Members._ID)) { cursor ->
|
||||||
) { cursor ->
|
|
||||||
val songIdIndex =
|
val songIdIndex =
|
||||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID)
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID)
|
||||||
|
|
||||||
|
@ -292,15 +291,14 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
|
||||||
MediaStore.Audio.AudioColumns.ALBUM,
|
MediaStore.Audio.AudioColumns.ALBUM,
|
||||||
MediaStore.Audio.AudioColumns.ALBUM_ID,
|
MediaStore.Audio.AudioColumns.ALBUM_ID,
|
||||||
MediaStore.Audio.AudioColumns.ARTIST,
|
MediaStore.Audio.AudioColumns.ARTIST,
|
||||||
AUDIO_COLUMN_ALBUM_ARTIST
|
AUDIO_COLUMN_ALBUM_ARTIST)
|
||||||
)
|
|
||||||
|
|
||||||
protected abstract val dirSelector: String
|
protected abstract val dirSelector: String
|
||||||
protected abstract fun addDirToSelectorArgs(dir: Directory, args: MutableList<String>): Boolean
|
protected abstract fun addDirToSelectorArgs(dir: Directory, args: MutableList<String>): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populate the "file data" of the cursor, or data that is required to access a cache entry
|
* Populate the "file data" of the cursor, or data that is required to access a cache entry or
|
||||||
* or makes no sense to cache. This includes database IDs, modification dates,
|
* makes no sense to cache. This includes database IDs, modification dates,
|
||||||
*/
|
*/
|
||||||
protected open fun populateFileData(cursor: Cursor, raw: Song.Raw) {
|
protected open fun populateFileData(cursor: Cursor, raw: Song.Raw) {
|
||||||
raw.mediaStoreId = cursor.getLong(idIndex)
|
raw.mediaStoreId = cursor.getLong(idIndex)
|
||||||
|
@ -315,9 +313,7 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
|
||||||
raw.albumMediaStoreId = cursor.getLong(albumIdIndex)
|
raw.albumMediaStoreId = cursor.getLong(albumIdIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Extract cursor metadata into [raw]. */
|
||||||
* Extract cursor metadata into [raw].
|
|
||||||
*/
|
|
||||||
protected open fun populateMetadata(cursor: Cursor, raw: Song.Raw) {
|
protected open fun populateMetadata(cursor: Cursor, raw: Song.Raw) {
|
||||||
raw.name = cursor.getString(titleIndex)
|
raw.name = cursor.getString(titleIndex)
|
||||||
|
|
||||||
|
@ -362,8 +358,7 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
|
||||||
* External has existed since at least API 21, but no constant existed for it until API 29.
|
* External has existed since at least API 21, but no constant existed for it until API 29.
|
||||||
* This constant is safe to use.
|
* This constant is safe to use.
|
||||||
*/
|
*/
|
||||||
@Suppress("InlinedApi")
|
@Suppress("InlinedApi") private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL
|
||||||
private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base selector that works across all versions of android. Does not exclude
|
* The base selector that works across all versions of android. Does not exclude
|
||||||
|
@ -377,8 +372,8 @@ abstract class MediaStoreExtractor(private val context: Context, private val cac
|
||||||
// speed, we only want to add redundancy on known issues, not with possible issues.
|
// speed, we only want to add redundancy on known issues, not with possible issues.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [MediaStoreExtractor] that completes the music loading process in a way compatible from
|
* A [MediaStoreExtractor] that completes the music loading process in a way compatible from API 21
|
||||||
* API 21 onwards to API 29.
|
* onwards to API 29.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class Api21MediaStoreExtractor(context: Context, cacheDatabase: CacheExtractor) :
|
class Api21MediaStoreExtractor(context: Context, cacheDatabase: CacheExtractor) :
|
||||||
|
@ -471,8 +466,7 @@ open class BaseApi29MediaStoreExtractor(context: Context, cacheDatabase: CacheEx
|
||||||
super.projection +
|
super.projection +
|
||||||
arrayOf(
|
arrayOf(
|
||||||
MediaStore.Audio.AudioColumns.VOLUME_NAME,
|
MediaStore.Audio.AudioColumns.VOLUME_NAME,
|
||||||
MediaStore.Audio.AudioColumns.RELATIVE_PATH
|
MediaStore.Audio.AudioColumns.RELATIVE_PATH)
|
||||||
)
|
|
||||||
|
|
||||||
override val dirSelector: String
|
override val dirSelector: String
|
||||||
get() =
|
get() =
|
||||||
|
@ -502,8 +496,8 @@ open class BaseApi29MediaStoreExtractor(context: Context, cacheDatabase: CacheEx
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [MediaStoreExtractor] that completes the music loading process in a way compatible with at least
|
* A [MediaStoreExtractor] that completes the music loading process in a way compatible with at
|
||||||
* API 29.
|
* least API 29.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
@ -534,8 +528,8 @@ open class Api29MediaStoreExtractor(context: Context, cacheDatabase: CacheExtrac
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [MediaStoreExtractor] that completes the music loading process in a way compatible with at least
|
* A [MediaStoreExtractor] that completes the music loading process in a way compatible with at
|
||||||
* API 30.
|
* least API 30.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
@ -556,8 +550,7 @@ class Api30MediaStoreExtractor(context: Context, cacheDatabase: CacheExtractor)
|
||||||
super.projection +
|
super.projection +
|
||||||
arrayOf(
|
arrayOf(
|
||||||
MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER,
|
MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER,
|
||||||
MediaStore.Audio.AudioColumns.DISC_NUMBER
|
MediaStore.Audio.AudioColumns.DISC_NUMBER)
|
||||||
)
|
|
||||||
|
|
||||||
override fun populateMetadata(cursor: Cursor, raw: Song.Raw) {
|
override fun populateMetadata(cursor: Cursor, raw: Song.Raw) {
|
||||||
super.populateMetadata(cursor, raw)
|
super.populateMetadata(cursor, raw)
|
||||||
|
|
|
@ -45,7 +45,10 @@ import org.oxycblt.auxio.util.logW
|
||||||
*
|
*
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class MetadataExtractor(private val context: Context, private val mediaStoreExtractor: MediaStoreExtractor) {
|
class MetadataExtractor(
|
||||||
|
private val context: Context,
|
||||||
|
private val mediaStoreExtractor: MediaStoreExtractor
|
||||||
|
) {
|
||||||
private val taskPool: Array<Task?> = arrayOfNulls(TASK_CAPACITY)
|
private val taskPool: Array<Task?> = arrayOfNulls(TASK_CAPACITY)
|
||||||
|
|
||||||
/** Initialize the sub-layers that this layer relies on. */
|
/** Initialize the sub-layers that this layer relies on. */
|
||||||
|
@ -116,8 +119,7 @@ class Task(context: Context, private val raw: Song.Raw) {
|
||||||
private val future =
|
private val future =
|
||||||
MetadataRetriever.retrieveMetadata(
|
MetadataRetriever.retrieveMetadata(
|
||||||
context,
|
context,
|
||||||
MediaItem.fromUri(requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.audioUri)
|
MediaItem.fromUri(requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.audioUri))
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the song that this task is trying to complete. If the task is still busy, this will
|
* Get the song that this task is trying to complete. If the task is still busy, this will
|
||||||
|
@ -215,20 +217,16 @@ class Task(context: Context, private val raw: Song.Raw) {
|
||||||
// 3. ID3v2.4 Release Date, as it is the second most common date type
|
// 3. ID3v2.4 Release Date, as it is the second most common date type
|
||||||
// 4. ID3v2.3 Original Date, as it is like #1
|
// 4. ID3v2.3 Original Date, as it is like #1
|
||||||
// 5. ID3v2.3 Release Year, as it is the most common date type
|
// 5. ID3v2.3 Release Year, as it is the most common date type
|
||||||
(
|
(tags["TDOR"]?.run { get(0).parseTimestamp() }
|
||||||
tags["TDOR"]?.run { get(0).parseTimestamp() }
|
|
||||||
?: tags["TDRC"]?.run { get(0).parseTimestamp() }
|
?: tags["TDRC"]?.run { get(0).parseTimestamp() }
|
||||||
?: tags["TDRL"]?.run { get(0).parseTimestamp() } ?: parseId3v23Date(tags)
|
?: tags["TDRL"]?.run { get(0).parseTimestamp() } ?: parseId3v23Date(tags))
|
||||||
)
|
|
||||||
?.let { raw.date = it }
|
?.let { raw.date = it }
|
||||||
|
|
||||||
// Album
|
// Album
|
||||||
tags["TXXX:MusicBrainz Album Id"]?.let { raw.albumMusicBrainzId = it[0] }
|
tags["TXXX:MusicBrainz Album Id"]?.let { raw.albumMusicBrainzId = it[0] }
|
||||||
tags["TALB"]?.let { raw.albumName = it[0] }
|
tags["TALB"]?.let { raw.albumName = it[0] }
|
||||||
tags["TSOA"]?.let { raw.albumSortName = it[0] }
|
tags["TSOA"]?.let { raw.albumSortName = it[0] }
|
||||||
(tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.let {
|
(tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.let { raw.albumReleaseTypes = it }
|
||||||
raw.albumReleaseTypes = it
|
|
||||||
}
|
|
||||||
|
|
||||||
// Artist
|
// Artist
|
||||||
tags["TXXX:MusicBrainz Artist Id"]?.let { raw.artistMusicBrainzIds = it }
|
tags["TXXX:MusicBrainz Artist Id"]?.let { raw.artistMusicBrainzIds = it }
|
||||||
|
@ -288,11 +286,9 @@ class Task(context: Context, private val raw: Song.Raw) {
|
||||||
// 2. Date, as it is the most common date type
|
// 2. Date, as it is the most common date type
|
||||||
// 3. Year, as old vorbis tags tended to use this (I know this because it's the only
|
// 3. Year, as old vorbis tags tended to use this (I know this because it's the only
|
||||||
// tag that android supports, so it must be 15 years old or more!)
|
// tag that android supports, so it must be 15 years old or more!)
|
||||||
(
|
(tags["ORIGINALDATE"]?.run { get(0).parseTimestamp() }
|
||||||
tags["ORIGINALDATE"]?.run { get(0).parseTimestamp() }
|
|
||||||
?: tags["DATE"]?.run { get(0).parseTimestamp() }
|
?: tags["DATE"]?.run { get(0).parseTimestamp() }
|
||||||
?: tags["YEAR"]?.run { get(0).parseYear() }
|
?: tags["YEAR"]?.run { get(0).parseYear() })
|
||||||
)
|
|
||||||
?.let { raw.date = it }
|
?.let { raw.date = it }
|
||||||
|
|
||||||
// Album
|
// Album
|
||||||
|
|
|
@ -18,10 +18,10 @@
|
||||||
package org.oxycblt.auxio.music.extractor
|
package org.oxycblt.auxio.music.extractor
|
||||||
|
|
||||||
import androidx.core.text.isDigitsOnly
|
import androidx.core.text.isDigitsOnly
|
||||||
|
import java.util.UUID
|
||||||
import org.oxycblt.auxio.music.Date
|
import org.oxycblt.auxio.music.Date
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse out the track number field as if the given Int is formatted as DTTT, where D Is the disc
|
* Parse out the track number field as if the given Int is formatted as DTTT, where D Is the disc
|
||||||
|
@ -51,9 +51,7 @@ fun String.parseYear() = toIntOrNull()?.toDate()
|
||||||
/** Parse an ISO-8601 time-stamp from this field into a [Date]. */
|
/** Parse an ISO-8601 time-stamp from this field into a [Date]. */
|
||||||
fun String.parseTimestamp() = Date.from(this)
|
fun String.parseTimestamp() = Date.from(this)
|
||||||
|
|
||||||
/**
|
/** Parse a string by [selector], also handling string escaping. */
|
||||||
* Parse a string by [selector], also handling string escaping.
|
|
||||||
*/
|
|
||||||
inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList<String> {
|
inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList<String> {
|
||||||
val split = mutableListOf<String>()
|
val split = mutableListOf<String>()
|
||||||
var currentString = ""
|
var currentString = ""
|
||||||
|
@ -110,11 +108,12 @@ fun String.maybeParseSeparators(settings: Settings): List<String> {
|
||||||
return splitEscaped { separators.contains(it) }
|
return splitEscaped { separators.contains(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun String.toUuidOrNull(): UUID? = try {
|
fun String.toUuidOrNull(): UUID? =
|
||||||
UUID.fromString(this)
|
try {
|
||||||
} catch (e: IllegalArgumentException) {
|
UUID.fromString(this)
|
||||||
null
|
} catch (e: IllegalArgumentException) {
|
||||||
}
|
null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a multi-value genre name using ID3v2 rules. If there is one value, the ID3v2.3 rules will
|
* Parse a multi-value genre name using ID3v2 rules. If there is one value, the ID3v2.3 rules will
|
||||||
|
@ -392,5 +391,4 @@ private val GENRE_TABLE =
|
||||||
"Psybient",
|
"Psybient",
|
||||||
|
|
||||||
// Auxio's extensions (Future garage is also based and deserves a slot)
|
// Auxio's extensions (Future garage is also based and deserves a slot)
|
||||||
"Future Garage"
|
"Future Garage")
|
||||||
)
|
|
||||||
|
|
|
@ -27,10 +27,9 @@ import org.oxycblt.auxio.ui.recycler.ItemClickListener
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
|
|
||||||
/**
|
/** The adapter that displays a list of artist choices in the picker UI. */
|
||||||
* The adapter that displays a list of artist choices in the picker UI.
|
class ArtistChoiceAdapter(private val listener: ItemClickListener) :
|
||||||
*/
|
RecyclerView.Adapter<ArtistChoiceViewHolder>() {
|
||||||
class ArtistChoiceAdapter(private val listener: ItemClickListener) : RecyclerView.Adapter<ArtistChoiceViewHolder>() {
|
|
||||||
private var artists = listOf<Artist>()
|
private var artists = listOf<Artist>()
|
||||||
|
|
||||||
override fun getItemCount() = artists.size
|
override fun getItemCount() = artists.size
|
||||||
|
@ -45,8 +44,7 @@ class ArtistChoiceAdapter(private val listener: ItemClickListener) : RecyclerVie
|
||||||
if (newArtists != artists) {
|
if (newArtists != artists) {
|
||||||
artists = newArtists
|
artists = newArtists
|
||||||
|
|
||||||
@Suppress("NotifyDataSetChanged")
|
@Suppress("NotifyDataSetChanged") notifyDataSetChanged()
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,13 +53,12 @@ class ArtistChoiceAdapter(private val listener: ItemClickListener) : RecyclerVie
|
||||||
* The ViewHolder that displays a artist choice. Smaller than other parent items due to dialog
|
* The ViewHolder that displays a artist choice. Smaller than other parent items due to dialog
|
||||||
* constraints.
|
* constraints.
|
||||||
*/
|
*/
|
||||||
class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) : DialogViewHolder(binding.root) {
|
class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) :
|
||||||
|
DialogViewHolder(binding.root) {
|
||||||
fun bind(artist: Artist, listener: ItemClickListener) {
|
fun bind(artist: Artist, listener: ItemClickListener) {
|
||||||
binding.pickerImage.bind(artist)
|
binding.pickerImage.bind(artist)
|
||||||
binding.pickerName.text = artist.resolveName(binding.context)
|
binding.pickerName.text = artist.resolveName(binding.context)
|
||||||
binding.root.setOnClickListener {
|
binding.root.setOnClickListener { listener.onItemClick(artist) }
|
||||||
listener.onItemClick(artist)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -44,7 +44,8 @@ import org.oxycblt.auxio.util.collectImmediately
|
||||||
*
|
*
|
||||||
* TODO: Clean up the picker flow to reduce the amount of duplication I had to do.
|
* TODO: Clean up the picker flow to reduce the amount of duplication I had to do.
|
||||||
*/
|
*/
|
||||||
class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>(), ItemClickListener {
|
class ArtistPickerDialog :
|
||||||
|
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ItemClickListener {
|
||||||
private val pickerModel: PickerViewModel by viewModels()
|
private val pickerModel: PickerViewModel by viewModels()
|
||||||
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
||||||
private val navModel: NavigationViewModel by activityViewModels()
|
private val navModel: NavigationViewModel by activityViewModels()
|
||||||
|
@ -56,9 +57,7 @@ class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>()
|
||||||
DialogMusicPickerBinding.inflate(inflater)
|
DialogMusicPickerBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onConfigDialog(builder: AlertDialog.Builder) {
|
override fun onConfigDialog(builder: AlertDialog.Builder) {
|
||||||
builder
|
builder.setTitle(R.string.lbl_artists).setNegativeButton(R.string.lbl_cancel, null)
|
||||||
.setTitle(R.string.lbl_artists)
|
|
||||||
.setNegativeButton(R.string.lbl_cancel, null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
|
||||||
|
|
|
@ -17,9 +17,7 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.picker
|
package org.oxycblt.auxio.music.picker
|
||||||
|
|
||||||
/**
|
/** Represents the actions available to the picker UI. */
|
||||||
* Represents the actions available to the picker UI.
|
|
||||||
*/
|
|
||||||
enum class PickerMode {
|
enum class PickerMode {
|
||||||
PLAY,
|
PLAY,
|
||||||
SHOW
|
SHOW
|
||||||
|
|
|
@ -98,8 +98,7 @@ class MusicDirsDialog :
|
||||||
dirs =
|
dirs =
|
||||||
MusicDirs(
|
MusicDirs(
|
||||||
pendingDirs.mapNotNull { Directory.fromDocumentUri(storageManager, it) },
|
pendingDirs.mapNotNull { Directory.fromDocumentUri(storageManager, it) },
|
||||||
savedInstanceState.getBoolean(KEY_PENDING_MODE)
|
savedInstanceState.getBoolean(KEY_PENDING_MODE))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,8 +111,7 @@ class MusicDirsDialog :
|
||||||
R.id.dirs_mode_include
|
R.id.dirs_mode_include
|
||||||
} else {
|
} else {
|
||||||
R.id.dirs_mode_exclude
|
R.id.dirs_mode_exclude
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
updateMode()
|
updateMode()
|
||||||
addOnButtonCheckedListener { _, _, _ -> updateMode() }
|
addOnButtonCheckedListener { _, _, _ -> updateMode() }
|
||||||
|
@ -123,9 +121,7 @@ class MusicDirsDialog :
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
outState.putStringArrayList(
|
outState.putStringArrayList(
|
||||||
KEY_PENDING_DIRS,
|
KEY_PENDING_DIRS, ArrayList(dirAdapter.dirs.map { it.toString() }))
|
||||||
ArrayList(dirAdapter.dirs.map { it.toString() })
|
|
||||||
)
|
|
||||||
outState.putBoolean(KEY_PENDING_MODE, isInclude(requireBinding()))
|
outState.putBoolean(KEY_PENDING_MODE, isInclude(requireBinding()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,9 +155,7 @@ class MusicDirsDialog :
|
||||||
// Turn the raw URI into a document tree URI
|
// Turn the raw URI into a document tree URI
|
||||||
val docUri =
|
val docUri =
|
||||||
DocumentsContract.buildDocumentUriUsingTree(
|
DocumentsContract.buildDocumentUriUsingTree(
|
||||||
uri,
|
uri, DocumentsContract.getTreeDocumentId(uri))
|
||||||
DocumentsContract.getTreeDocumentId(uri)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Turn it into a semi-usable path
|
// Turn it into a semi-usable path
|
||||||
val treeUri = DocumentsContract.getTreeDocumentId(docUri)
|
val treeUri = DocumentsContract.getTreeDocumentId(docUri)
|
||||||
|
|
|
@ -30,10 +30,10 @@ import android.os.storage.StorageVolume
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import com.google.android.exoplayer2.util.MimeTypes
|
import com.google.android.exoplayer2.util.MimeTypes
|
||||||
import org.oxycblt.auxio.R
|
|
||||||
import org.oxycblt.auxio.util.lazyReflectedMethod
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.util.lazyReflectedMethod
|
||||||
|
|
||||||
/** A path to a file. [name] is the stripped file name, [parent] is the parent path. */
|
/** A path to a file. [name] is the stripped file name, [parent] is the parent path. */
|
||||||
data class Path(val name: String, val parent: Directory)
|
data class Path(val name: String, val parent: Directory)
|
||||||
|
@ -70,9 +70,7 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
|
||||||
|
|
||||||
fun from(volume: StorageVolume, relativePath: String) =
|
fun from(volume: StorageVolume, relativePath: String) =
|
||||||
Directory(
|
Directory(
|
||||||
volume,
|
volume, relativePath.removePrefix(File.separator).removeSuffix(File.separator))
|
||||||
relativePath.removePrefix(File.separator).removeSuffix(File.separator)
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts an opaque document uri in the form of VOLUME:PATH into a [Directory]. This is a
|
* Converts an opaque document uri in the form of VOLUME:PATH into a [Directory]. This is a
|
||||||
|
@ -198,15 +196,14 @@ val Long.albumCoverUri: Uri
|
||||||
|
|
||||||
@Suppress("NewApi")
|
@Suppress("NewApi")
|
||||||
private val SM_API21_GET_VOLUME_LIST_METHOD: Method by
|
private val SM_API21_GET_VOLUME_LIST_METHOD: Method by
|
||||||
lazyReflectedMethod(StorageManager::class, "getVolumeList")
|
lazyReflectedMethod(StorageManager::class, "getVolumeList")
|
||||||
|
|
||||||
@Suppress("NewApi")
|
@Suppress("NewApi")
|
||||||
private val SV_API21_GET_PATH_METHOD: Method by lazyReflectedMethod(StorageVolume::class, "getPath")
|
private val SV_API21_GET_PATH_METHOD: Method by lazyReflectedMethod(StorageVolume::class, "getPath")
|
||||||
|
|
||||||
/** The "primary" storage volume containing the OS. May be an SD Card. */
|
/** The "primary" storage volume containing the OS. May be an SD Card. */
|
||||||
val StorageManager.primaryStorageVolumeCompat: StorageVolume
|
val StorageManager.primaryStorageVolumeCompat: StorageVolume
|
||||||
@Suppress("NewApi")
|
@Suppress("NewApi") get() = primaryStorageVolume
|
||||||
get() = primaryStorageVolume
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of recognized volumes, retrieved in a compatible manner. Note that these volumes may be
|
* A list of recognized volumes, retrieved in a compatible manner. Note that these volumes may be
|
||||||
|
@ -243,13 +240,11 @@ fun StorageVolume.getDescriptionCompat(context: Context): String = getDescriptio
|
||||||
|
|
||||||
/** If this volume is the primary volume. May still be removable storage. */
|
/** If this volume is the primary volume. May still be removable storage. */
|
||||||
val StorageVolume.isPrimaryCompat: Boolean
|
val StorageVolume.isPrimaryCompat: Boolean
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi") get() = isPrimary
|
||||||
get() = isPrimary
|
|
||||||
|
|
||||||
/** If this volume is emulated. */
|
/** If this volume is emulated. */
|
||||||
val StorageVolume.isEmulatedCompat: Boolean
|
val StorageVolume.isEmulatedCompat: Boolean
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi") get() = isEmulated
|
||||||
get() = isEmulated
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If this volume corresponds to "Internal shared storage", represented in document URIs as
|
* If this volume corresponds to "Internal shared storage", represented in document URIs as
|
||||||
|
@ -260,13 +255,11 @@ val StorageVolume.isInternalCompat: Boolean
|
||||||
|
|
||||||
/** Returns the UUID of the volume in a compatible manner. */
|
/** Returns the UUID of the volume in a compatible manner. */
|
||||||
val StorageVolume.uuidCompat: String?
|
val StorageVolume.uuidCompat: String?
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi") get() = uuid
|
||||||
get() = uuid
|
|
||||||
|
|
||||||
/** Returns the state of the volume in a compatible manner. */
|
/** Returns the state of the volume in a compatible manner. */
|
||||||
val StorageVolume.stateCompat: String
|
val StorageVolume.stateCompat: String
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi") get() = state
|
||||||
get() = state
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the name of this volume as it is used in [MediaStore]. This will be
|
* Returns the name of this volume as it is used in [MediaStore]. This will be
|
||||||
|
|
|
@ -133,9 +133,9 @@ class Indexer {
|
||||||
/**
|
/**
|
||||||
* Start the indexing process. This should be done by [Controller] in a background thread. When
|
* Start the indexing process. This should be done by [Controller] in a background thread. When
|
||||||
* complete, a new completion state will be pushed to each callback.
|
* complete, a new completion state will be pushed to each callback.
|
||||||
* @param fresh Whether to use the cache when loading.
|
* @param withCache Whether to use the cache when loading.
|
||||||
*/
|
*/
|
||||||
suspend fun index(context: Context, fresh: Boolean) {
|
suspend fun index(context: Context, withCache: Boolean) {
|
||||||
val notGranted =
|
val notGranted =
|
||||||
ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) ==
|
ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) ==
|
||||||
PackageManager.PERMISSION_DENIED
|
PackageManager.PERMISSION_DENIED
|
||||||
|
@ -148,12 +148,11 @@ class Indexer {
|
||||||
val response =
|
val response =
|
||||||
try {
|
try {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
val library = indexImpl(context, fresh)
|
val library = indexImpl(context, withCache)
|
||||||
if (library != null) {
|
if (library != null) {
|
||||||
logD(
|
logD(
|
||||||
"Music indexing completed successfully in " +
|
"Music indexing completed successfully in " +
|
||||||
"${System.currentTimeMillis() - start}ms"
|
"${System.currentTimeMillis() - start}ms")
|
||||||
)
|
|
||||||
Response.Ok(library)
|
Response.Ok(library)
|
||||||
} else {
|
} else {
|
||||||
logE("No music found")
|
logE("No music found")
|
||||||
|
@ -194,9 +193,7 @@ class Indexer {
|
||||||
emitIndexing(null)
|
emitIndexing(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Run the proper music loading process. */
|
||||||
* Run the proper music loading process.
|
|
||||||
*/
|
|
||||||
private suspend fun indexImpl(context: Context, withCache: Boolean): MusicStore.Library? {
|
private suspend fun indexImpl(context: Context, withCache: Boolean): MusicStore.Library? {
|
||||||
// Create the chain of extractors. Each extractor builds on the previous and
|
// Create the chain of extractors. Each extractor builds on the previous and
|
||||||
// enables version-specific features in order to create the best possible music
|
// enables version-specific features in order to create the best possible music
|
||||||
|
@ -250,12 +247,15 @@ class Indexer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does the initial query over the song database using [metadataExtractor]. The songs returned by
|
* Does the initial query over the song database using [metadataExtractor]. The songs returned
|
||||||
* this function are **not** well-formed. The companion [buildAlbums], [buildArtists], and
|
* by this function are **not** well-formed. The companion [buildAlbums], [buildArtists], and
|
||||||
* [buildGenres] functions must be called with the returned list so that all songs are properly
|
* [buildGenres] functions must be called with the returned list so that all songs are properly
|
||||||
* linked up.
|
* linked up.
|
||||||
*/
|
*/
|
||||||
private suspend fun buildSongs(metadataExtractor: MetadataExtractor, settings: Settings): List<Song> {
|
private suspend fun buildSongs(
|
||||||
|
metadataExtractor: MetadataExtractor,
|
||||||
|
settings: Settings
|
||||||
|
): List<Song> {
|
||||||
logD("Starting indexing process")
|
logD("Starting indexing process")
|
||||||
|
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
|
@ -317,9 +317,9 @@ class Indexer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Group up songs AND albums into artists. This process seems weird (because it is), but
|
* Group up songs AND albums into artists. This process seems weird (because it is), but the
|
||||||
* the purpose is that the actual artist information of albums and songs often differs,
|
* purpose is that the actual artist information of albums and songs often differs, and so they
|
||||||
* and so they are linked in different ways.
|
* are linked in different ways.
|
||||||
*/
|
*/
|
||||||
private fun buildArtists(songs: List<Song>, albums: List<Album>): List<Artist> {
|
private fun buildArtists(songs: List<Song>, albums: List<Album>): List<Artist> {
|
||||||
val musicByArtist = mutableMapOf<Artist.Raw, MutableList<Music>>()
|
val musicByArtist = mutableMapOf<Artist.Raw, MutableList<Music>>()
|
||||||
|
|
|
@ -67,8 +67,7 @@ class IndexingNotification(private val context: Context) :
|
||||||
// Only update the notification every 1.5s to prevent rate-limiting.
|
// Only update the notification every 1.5s to prevent rate-limiting.
|
||||||
logD("Updating state to $indexing")
|
logD("Updating state to $indexing")
|
||||||
setContentText(
|
setContentText(
|
||||||
context.getString(R.string.fmt_indexing, indexing.current, indexing.total)
|
context.getString(R.string.fmt_indexing, indexing.current, indexing.total))
|
||||||
)
|
|
||||||
setProgress(indexing.total, indexing.current, false)
|
setProgress(indexing.total, indexing.current, false)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -95,6 +94,4 @@ class ObservingNotification(context: Context) : ServiceNotification(context, IND
|
||||||
|
|
||||||
private val INDEXER_CHANNEL =
|
private val INDEXER_CHANNEL =
|
||||||
ServiceNotification.ChannelInfo(
|
ServiceNotification.ChannelInfo(
|
||||||
id = BuildConfig.APPLICATION_ID + ".channel.INDEXER",
|
id = BuildConfig.APPLICATION_ID + ".channel.INDEXER", nameRes = R.string.lbl_indexer)
|
||||||
nameRes = R.string.lbl_indexer
|
|
||||||
)
|
|
||||||
|
|
|
@ -79,9 +79,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
wakeLock =
|
wakeLock =
|
||||||
getSystemServiceCompat(PowerManager::class)
|
getSystemServiceCompat(PowerManager::class)
|
||||||
.newWakeLock(
|
.newWakeLock(
|
||||||
PowerManager.PARTIAL_WAKE_LOCK,
|
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":IndexerService")
|
||||||
BuildConfig.APPLICATION_ID + ":IndexerService"
|
|
||||||
)
|
|
||||||
|
|
||||||
settings = Settings(this, this)
|
settings = Settings(this, this)
|
||||||
indexerContentObserver = SystemContentObserver()
|
indexerContentObserver = SystemContentObserver()
|
||||||
|
@ -130,8 +128,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
when (state) {
|
when (state) {
|
||||||
is Indexer.State.Complete -> {
|
is Indexer.State.Complete -> {
|
||||||
if (state.response is Indexer.Response.Ok &&
|
if (state.response is Indexer.Response.Ok &&
|
||||||
state.response.library != musicStore.library
|
state.response.library != musicStore.library) {
|
||||||
) {
|
|
||||||
logD("Applying new library")
|
logD("Applying new library")
|
||||||
|
|
||||||
val newLibrary = state.response.library
|
val newLibrary = state.response.library
|
||||||
|
@ -243,10 +240,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
) : ContentObserver(handler), Runnable {
|
) : ContentObserver(handler), Runnable {
|
||||||
init {
|
init {
|
||||||
contentResolverSafe.registerContentObserver(
|
contentResolverSafe.registerContentObserver(
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, this)
|
||||||
true,
|
|
||||||
this
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun release() {
|
fun release() {
|
||||||
|
@ -263,8 +257,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
// Check here if we should even start a reindex. This is much less bug-prone than
|
// Check here if we should even start a reindex. This is much less bug-prone than
|
||||||
// registering and de-registering this component as this setting changes.
|
// registering and de-registering this component as this setting changes.
|
||||||
if (settings.shouldBeObserving) {
|
if (settings.shouldBeObserving) {
|
||||||
onSt
|
onStartIndexing(true)
|
||||||
artIndexing(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,11 +26,12 @@ enum class ActionMode {
|
||||||
SHUFFLE;
|
SHUFFLE;
|
||||||
|
|
||||||
val intCode: Int
|
val intCode: Int
|
||||||
get() = when (this) {
|
get() =
|
||||||
NEXT -> IntegerTable.ACTION_MODE_NEXT
|
when (this) {
|
||||||
REPEAT -> IntegerTable.ACTION_MODE_REPEAT
|
NEXT -> IntegerTable.ACTION_MODE_NEXT
|
||||||
SHUFFLE -> IntegerTable.ACTION_MODE_SHUFFLE
|
REPEAT -> IntegerTable.ACTION_MODE_REPEAT
|
||||||
}
|
SHUFFLE -> IntegerTable.ACTION_MODE_SHUFFLE
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** Convert an int [code] into an instance, or null if it isn't valid. */
|
/** Convert an int [code] into an instance, or null if it isn't valid. */
|
||||||
|
|
|
@ -89,16 +89,12 @@ class PlaybackPanelFragment :
|
||||||
|
|
||||||
binding.playbackArtist.apply {
|
binding.playbackArtist.apply {
|
||||||
isSelected = true
|
isSelected = true
|
||||||
setOnClickListener {
|
setOnClickListener { playbackModel.song.value?.let { showCurrentArtist() } }
|
||||||
playbackModel.song.value?.let { showCurrentArtist() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.playbackAlbum.apply {
|
binding.playbackAlbum.apply {
|
||||||
isSelected = true
|
isSelected = true
|
||||||
setOnClickListener {
|
setOnClickListener { playbackModel.song.value?.let { showCurrentAlbum() } }
|
||||||
playbackModel.song.value?.let { showCurrentAlbum() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.playbackSeekBar.callback = this
|
binding.playbackSeekBar.callback = this
|
||||||
|
@ -136,9 +132,7 @@ class PlaybackPanelFragment :
|
||||||
val equalizerIntent =
|
val equalizerIntent =
|
||||||
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL)
|
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL)
|
||||||
.putExtra(
|
.putExtra(
|
||||||
AudioEffect.EXTRA_AUDIO_SESSION,
|
AudioEffect.EXTRA_AUDIO_SESSION, playbackModel.currentAudioSessionId)
|
||||||
playbackModel.currentAudioSessionId
|
|
||||||
)
|
|
||||||
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
|
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -161,9 +155,7 @@ class PlaybackPanelFragment :
|
||||||
playbackModel.song.value?.let { song ->
|
playbackModel.song.value?.let { song ->
|
||||||
navModel.mainNavigateTo(
|
navModel.mainNavigateTo(
|
||||||
MainNavigationAction.Directions(
|
MainNavigationAction.Directions(
|
||||||
MainFragmentDirections.actionShowDetails(song.uid)
|
MainFragmentDirections.actionShowDetails(song.uid)))
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|
|
@ -60,7 +60,5 @@ class PlaybackSheetBehavior<V : View>(context: Context, attributeSet: AttributeS
|
||||||
MaterialShapeDrawable(sheetBackgroundDrawable.shapeAppearanceModel).apply {
|
MaterialShapeDrawable(sheetBackgroundDrawable.shapeAppearanceModel).apply {
|
||||||
fillColor = sheetBackgroundDrawable.fillColor
|
fillColor = sheetBackgroundDrawable.fillColor
|
||||||
},
|
},
|
||||||
sheetBackgroundDrawable
|
sheetBackgroundDrawable))
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,44 +115,32 @@ class PlaybackViewModel(application: Application) :
|
||||||
playbackManager.play(song, genre, settings)
|
playbackManager.play(song, genre, settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Play an [album]. */
|
||||||
* Play an [album].
|
|
||||||
*/
|
|
||||||
fun play(album: Album) {
|
fun play(album: Album) {
|
||||||
playbackManager.play(null, album, settings, false)
|
playbackManager.play(null, album, settings, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Play an [artist]. */
|
||||||
* Play an [artist].
|
|
||||||
*/
|
|
||||||
fun play(artist: Artist) {
|
fun play(artist: Artist) {
|
||||||
playbackManager.play(null, artist, settings, false)
|
playbackManager.play(null, artist, settings, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Play a [genre]. */
|
||||||
* Play a [genre].
|
|
||||||
*/
|
|
||||||
fun play(genre: Genre) {
|
fun play(genre: Genre) {
|
||||||
playbackManager.play(null, genre, settings, false)
|
playbackManager.play(null, genre, settings, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Shuffle an [album]. */
|
||||||
* Shuffle an [album].
|
|
||||||
*/
|
|
||||||
fun shuffle(album: Album) {
|
fun shuffle(album: Album) {
|
||||||
playbackManager.play(null, album, settings, true)
|
playbackManager.play(null, album, settings, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Shuffle an [artist]. */
|
||||||
* Shuffle an [artist].
|
|
||||||
*/
|
|
||||||
fun shuffle(artist: Artist) {
|
fun shuffle(artist: Artist) {
|
||||||
playbackManager.play(null, artist, settings, true)
|
playbackManager.play(null, artist, settings, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Shuffle a [genre]. */
|
||||||
* Shuffle a [genre].
|
|
||||||
*/
|
|
||||||
fun shuffle(genre: Genre) {
|
fun shuffle(genre: Genre) {
|
||||||
playbackManager.play(null, genre, settings, true)
|
playbackManager.play(null, genre, settings, true)
|
||||||
}
|
}
|
||||||
|
@ -249,8 +237,8 @@ class PlaybackViewModel(application: Application) :
|
||||||
// --- SAVE/RESTORE FUNCTIONS ---
|
// --- SAVE/RESTORE FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force save the current [PlaybackStateManager] state to the database. [onDone]
|
* Force save the current [PlaybackStateManager] state to the database. [onDone] will be called
|
||||||
* will be called with true if it was done, or false if an error occurred.
|
* with true if it was done, or false if an error occurred.
|
||||||
*/
|
*/
|
||||||
fun savePlaybackState(onDone: (Boolean) -> Unit) {
|
fun savePlaybackState(onDone: (Boolean) -> Unit) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|
|
@ -131,9 +131,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong
|
||||||
fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface)
|
fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface)
|
||||||
elevation = binding.context.getDimen(R.dimen.elevation_normal)
|
elevation = binding.context.getDimen(R.dimen.elevation_normal)
|
||||||
},
|
},
|
||||||
backgroundDrawable
|
backgroundDrawable))
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
|
|
@ -42,9 +42,7 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
|
||||||
val queueHolder = viewHolder as QueueSongViewHolder
|
val queueHolder = viewHolder as QueueSongViewHolder
|
||||||
return if (queueHolder.isEnabled) {
|
return if (queueHolder.isEnabled) {
|
||||||
makeFlag(
|
makeFlag(
|
||||||
ItemTouchHelper.ACTION_STATE_DRAG,
|
ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN) or
|
||||||
ItemTouchHelper.UP or ItemTouchHelper.DOWN
|
|
||||||
) or
|
|
||||||
makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START)
|
makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START)
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
|
@ -134,9 +132,7 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
|
||||||
target: RecyclerView.ViewHolder
|
target: RecyclerView.ViewHolder
|
||||||
) =
|
) =
|
||||||
playbackModel.moveQueueDataItems(
|
playbackModel.moveQueueDataItems(
|
||||||
viewHolder.bindingAdapterPosition,
|
viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
|
||||||
target.bindingAdapterPosition
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||||
playbackModel.removeQueueDataItem(viewHolder.bindingAdapterPosition)
|
playbackModel.removeQueueDataItem(viewHolder.bindingAdapterPosition)
|
||||||
|
|
|
@ -61,18 +61,13 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
invalidateDivider()
|
invalidateDivider()
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ----
|
// --- VIEWMODEL SETUP ----
|
||||||
|
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
queueModel.queue,
|
queueModel.queue, queueModel.index, playbackModel.isPlaying, ::updateQueue)
|
||||||
queueModel.index,
|
|
||||||
playbackModel.isPlaying,
|
|
||||||
::updateQueue
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentQueueBinding) {
|
override fun onDestroyBinding(binding: FragmentQueueBinding) {
|
||||||
|
@ -102,7 +97,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
|
||||||
|
|
||||||
binding.queueDivider.isInvisible =
|
binding.queueDivider.isInvisible =
|
||||||
(binding.queueRecycler.layoutManager as LinearLayoutManager)
|
(binding.queueRecycler.layoutManager as LinearLayoutManager)
|
||||||
.findFirstCompletelyVisibleItemPosition() < 1
|
.findFirstCompletelyVisibleItemPosition() < 1
|
||||||
|
|
||||||
queueModel.finishReplace()
|
queueModel.finishReplace()
|
||||||
|
|
||||||
|
@ -127,6 +122,6 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
binding.queueDivider.isInvisible =
|
binding.queueDivider.isInvisible =
|
||||||
(binding.queueRecycler.layoutManager as LinearLayoutManager)
|
(binding.queueRecycler.layoutManager as LinearLayoutManager)
|
||||||
.findFirstCompletelyVisibleItemPosition() < 1
|
.findFirstCompletelyVisibleItemPosition() < 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,10 +70,6 @@ class QueueSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?
|
||||||
val bars = insets.systemBarInsetsCompat
|
val bars = insets.systemBarInsetsCompat
|
||||||
expandedOffset = bars.top + barHeight + barSpacing
|
expandedOffset = bars.top + barHeight + barSpacing
|
||||||
return insets.replaceSystemBarInsetsCompat(
|
return insets.replaceSystemBarInsetsCompat(
|
||||||
bars.left,
|
bars.left, bars.top, bars.right, expandedOffset + bars.bottom)
|
||||||
bars.top,
|
|
||||||
bars.right,
|
|
||||||
expandedOffset + bars.bottom
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,8 +59,7 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
/** Remove a queue item using it's recyclerview adapter index. */
|
/** Remove a queue item using it's recyclerview adapter index. */
|
||||||
fun removeQueueDataItem(adapterIndex: Int) {
|
fun removeQueueDataItem(adapterIndex: Int) {
|
||||||
if (adapterIndex <= playbackManager.index ||
|
if (adapterIndex <= playbackManager.index ||
|
||||||
adapterIndex !in playbackManager.queue.indices
|
adapterIndex !in playbackManager.queue.indices) {
|
||||||
) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,12 +21,12 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import kotlin.math.abs
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.DialogPreAmpBinding
|
import org.oxycblt.auxio.databinding.DialogPreAmpBinding
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
|
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The dialog for customizing the ReplayGain pre-amp values.
|
* The dialog for customizing the ReplayGain pre-amp values.
|
||||||
|
@ -82,7 +82,4 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() {
|
||||||
getString(R.string.fmt_db_neg, abs(valueDb))
|
getString(R.string.fmt_db_neg, abs(valueDb))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,13 +24,13 @@ import com.google.android.exoplayer2.audio.BaseAudioProcessor
|
||||||
import com.google.android.exoplayer2.metadata.Metadata
|
import com.google.android.exoplayer2.metadata.Metadata
|
||||||
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame
|
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame
|
||||||
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment
|
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import kotlin.math.pow
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import kotlin.math.pow
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An [AudioProcessor] that handles ReplayGain values and their amplification of the audio stream.
|
* An [AudioProcessor] that handles ReplayGain values and their amplification of the audio stream.
|
||||||
|
|
|
@ -92,8 +92,7 @@ interface InternalPlayer {
|
||||||
// Not advancing, so don't move the position.
|
// Not advancing, so don't move the position.
|
||||||
0f
|
0f
|
||||||
},
|
},
|
||||||
creationTime
|
creationTime)
|
||||||
)
|
|
||||||
|
|
||||||
// Equality ignores the creation time to prevent functionally
|
// Equality ignores the creation time to prevent functionally
|
||||||
// identical states from being equal.
|
// identical states from being equal.
|
||||||
|
@ -120,8 +119,7 @@ interface InternalPlayer {
|
||||||
// main playing value is paused.
|
// main playing value is paused.
|
||||||
isPlaying && isAdvancing,
|
isPlaying && isAdvancing,
|
||||||
positionMs,
|
positionMs,
|
||||||
SystemClock.elapsedRealtime()
|
SystemClock.elapsedRealtime())
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -110,8 +110,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
queue = queue,
|
queue = queue,
|
||||||
positionMs = rawState.positionMs,
|
positionMs = rawState.positionMs,
|
||||||
repeatMode = rawState.repeatMode,
|
repeatMode = rawState.repeatMode,
|
||||||
isShuffled = rawState.isShuffled
|
isShuffled = rawState.isShuffled)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readRawState(): RawState? {
|
private fun readRawState(): RawState? {
|
||||||
|
@ -134,12 +133,11 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
index = cursor.getInt(indexIndex),
|
index = cursor.getInt(indexIndex),
|
||||||
positionMs = cursor.getLong(posIndex),
|
positionMs = cursor.getLong(posIndex),
|
||||||
repeatMode = RepeatMode.fromIntCode(cursor.getInt(repeatModeIndex))
|
repeatMode = RepeatMode.fromIntCode(cursor.getInt(repeatModeIndex))
|
||||||
?: RepeatMode.NONE,
|
?: RepeatMode.NONE,
|
||||||
isShuffled = cursor.getInt(shuffleIndex) == 1,
|
isShuffled = cursor.getInt(shuffleIndex) == 1,
|
||||||
songUid = Music.UID.fromString(cursor.getString(songUidIndex))
|
songUid = Music.UID.fromString(cursor.getString(songUidIndex))
|
||||||
?: return@queryAll null,
|
?: return@queryAll null,
|
||||||
parentUid = cursor.getString(parentUidIndex)?.let(Music.UID::fromString)
|
parentUid = cursor.getString(parentUidIndex)?.let(Music.UID::fromString))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,8 +173,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
repeatMode = state.repeatMode,
|
repeatMode = state.repeatMode,
|
||||||
isShuffled = state.isShuffled,
|
isShuffled = state.isShuffled,
|
||||||
songUid = state.queue[state.index].uid,
|
songUid = state.queue[state.index].uid,
|
||||||
parentUid = state.parent?.uid
|
parentUid = state.parent?.uid)
|
||||||
)
|
|
||||||
|
|
||||||
writeRawState(rawState)
|
writeRawState(rawState)
|
||||||
writeQueue(state.queue)
|
writeQueue(state.queue)
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback.state
|
package org.oxycblt.auxio.playback.state
|
||||||
|
|
||||||
|
import kotlin.math.max
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
|
@ -31,7 +32,6 @@ import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master class (and possible god object) for the playback state.
|
* Master class (and possible god object) for the playback state.
|
||||||
|
@ -268,11 +268,7 @@ class PlaybackStateManager private constructor() {
|
||||||
notifyShuffledChanged()
|
notifyShuffledChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun orderQueue(
|
private fun orderQueue(settings: Settings, shuffled: Boolean, keep: Song?) {
|
||||||
settings: Settings,
|
|
||||||
shuffled: Boolean,
|
|
||||||
keep: Song?
|
|
||||||
) {
|
|
||||||
val newIndex: Int
|
val newIndex: Int
|
||||||
|
|
||||||
if (shuffled) {
|
if (shuffled) {
|
||||||
|
@ -369,13 +365,14 @@ class PlaybackStateManager private constructor() {
|
||||||
|
|
||||||
val library = musicStore.library ?: return false
|
val library = musicStore.library ?: return false
|
||||||
val internalPlayer = internalPlayer ?: return false
|
val internalPlayer = internalPlayer ?: return false
|
||||||
val state = try {
|
val state =
|
||||||
withContext(Dispatchers.IO) { database.read(library) }
|
try {
|
||||||
} catch (e: Exception) {
|
withContext(Dispatchers.IO) { database.read(library) }
|
||||||
logE("Unable to restore playback state.")
|
} catch (e: Exception) {
|
||||||
logE(e.stackTraceToString())
|
logE("Unable to restore playback state.")
|
||||||
return false
|
logE(e.stackTraceToString())
|
||||||
}
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if (state != null && (!isInitialized || force)) {
|
if (state != null && (!isInitialized || force)) {
|
||||||
|
@ -484,8 +481,7 @@ class PlaybackStateManager private constructor() {
|
||||||
queue = _queue,
|
queue = _queue,
|
||||||
positionMs = playerState.calculateElapsedPosition(),
|
positionMs = playerState.calculateElapsedPosition(),
|
||||||
isShuffled = isShuffled,
|
isShuffled = isShuffled,
|
||||||
repeatMode = repeatMode
|
repeatMode = repeatMode)
|
||||||
)
|
|
||||||
|
|
||||||
// --- CALLBACKS ---
|
// --- CALLBACKS ---
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback.system
|
package org.oxycblt.auxio.playback.system
|
||||||
|
|
||||||
import android.bluetooth.BluetoothProfile
|
import android.bluetooth.BluetoothProfile
|
||||||
|
@ -6,18 +23,20 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [BroadcastReceiver] that handles connections from bluetooth headsets, starting playback if
|
* A [BroadcastReceiver] that handles connections from bluetooth headsets, starting playback if they
|
||||||
* they occur.
|
* occur.
|
||||||
* @author seijikun, OxygenCobalt
|
* @author seijikun, OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class BluetoothHeadsetReceiver : BroadcastReceiver() {
|
class BluetoothHeadsetReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
if (intent.action == android.bluetooth.BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED) {
|
if (intent.action == android.bluetooth.BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED) {
|
||||||
val newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTED)
|
val newState =
|
||||||
|
intent.getIntExtra(
|
||||||
|
BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTED)
|
||||||
if (newState == BluetoothProfile.STATE_CONNECTED) {
|
if (newState == BluetoothProfile.STATE_CONNECTED) {
|
||||||
// TODO: Initialize the service (Permission workflow must be figured out)
|
// TODO: Initialize the service (Permission workflow must be figured out)
|
||||||
// Perhaps move this to the internal receivers?
|
// Perhaps move this to the internal receivers?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,22 +140,19 @@ class MediaSessionComponent(private val context: Context, private val callback:
|
||||||
.putText(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
|
.putText(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
|
||||||
.putText(
|
.putText(
|
||||||
MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST,
|
MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST,
|
||||||
song.album.resolveArtistContents(context)
|
song.album.resolveArtistContents(context))
|
||||||
)
|
|
||||||
.putText(MediaMetadataCompat.METADATA_KEY_AUTHOR, artist)
|
.putText(MediaMetadataCompat.METADATA_KEY_AUTHOR, artist)
|
||||||
.putText(MediaMetadataCompat.METADATA_KEY_COMPOSER, artist)
|
.putText(MediaMetadataCompat.METADATA_KEY_COMPOSER, artist)
|
||||||
.putText(MediaMetadataCompat.METADATA_KEY_WRITER, artist)
|
.putText(MediaMetadataCompat.METADATA_KEY_WRITER, artist)
|
||||||
.putText(
|
.putText(
|
||||||
METADATA_KEY_PARENT,
|
METADATA_KEY_PARENT,
|
||||||
parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs)
|
parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs))
|
||||||
)
|
|
||||||
.putText(MediaMetadataCompat.METADATA_KEY_GENRE, song.resolveGenreContents(context))
|
.putText(MediaMetadataCompat.METADATA_KEY_GENRE, song.resolveGenreContents(context))
|
||||||
.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
||||||
.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, artist)
|
.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, artist)
|
||||||
.putText(
|
.putText(
|
||||||
MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
|
MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
|
||||||
parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs)
|
parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs))
|
||||||
)
|
|
||||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs)
|
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs)
|
||||||
|
|
||||||
song.track?.let {
|
song.track?.let {
|
||||||
|
@ -166,9 +163,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
|
||||||
builder.putLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER, it.toLong())
|
builder.putLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER, it.toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
song.date?.let {
|
song.date?.let { builder.putString(MediaMetadataCompat.METADATA_KEY_DATE, it.toString()) }
|
||||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DATE, it.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cover loading is a mess. Android expects you to provide a clean, easy URI for it to
|
// Cover loading is a mess. Android expects you to provide a clean, easy URI for it to
|
||||||
// leverage, but Auxio cannot do that as quality-of-life features like scaling or
|
// leverage, but Auxio cannot do that as quality-of-life features like scaling or
|
||||||
|
@ -192,8 +187,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
|
||||||
notification.updateMetadata(metadata)
|
notification.updateMetadata(metadata)
|
||||||
callback.onPostNotification(notification, PostingReason.METADATA)
|
callback.onPostNotification(notification, PostingReason.METADATA)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateQueue(queue: List<Song>) {
|
private fun updateQueue(queue: List<Song>) {
|
||||||
|
@ -230,8 +224,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
|
||||||
RepeatMode.NONE -> PlaybackStateCompat.REPEAT_MODE_NONE
|
RepeatMode.NONE -> PlaybackStateCompat.REPEAT_MODE_NONE
|
||||||
RepeatMode.TRACK -> PlaybackStateCompat.REPEAT_MODE_ONE
|
RepeatMode.TRACK -> PlaybackStateCompat.REPEAT_MODE_ONE
|
||||||
RepeatMode.ALL -> PlaybackStateCompat.REPEAT_MODE_ALL
|
RepeatMode.ALL -> PlaybackStateCompat.REPEAT_MODE_ALL
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
invalidateSecondaryAction()
|
invalidateSecondaryAction()
|
||||||
}
|
}
|
||||||
|
@ -242,8 +235,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
|
||||||
PlaybackStateCompat.SHUFFLE_MODE_ALL
|
PlaybackStateCompat.SHUFFLE_MODE_ALL
|
||||||
} else {
|
} else {
|
||||||
PlaybackStateCompat.SHUFFLE_MODE_NONE
|
PlaybackStateCompat.SHUFFLE_MODE_NONE
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
invalidateSecondaryAction()
|
invalidateSecondaryAction()
|
||||||
}
|
}
|
||||||
|
@ -328,8 +320,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
|
||||||
playbackManager.reshuffle(
|
playbackManager.reshuffle(
|
||||||
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL ||
|
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL ||
|
||||||
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP,
|
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP,
|
||||||
settings
|
settings)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSkipToQueueItem(id: Long) {
|
override fun onSkipToQueueItem(id: Long) {
|
||||||
|
@ -368,30 +359,29 @@ class MediaSessionComponent(private val context: Context, private val callback:
|
||||||
|
|
||||||
// Android 13+ leverages custom actions in the notification.
|
// Android 13+ leverages custom actions in the notification.
|
||||||
|
|
||||||
val extraAction = when (settings.notifAction) {
|
val extraAction =
|
||||||
ActionMode.SHUFFLE -> PlaybackStateCompat.CustomAction.Builder(
|
when (settings.notifAction) {
|
||||||
PlaybackService.ACTION_INVERT_SHUFFLE,
|
ActionMode.SHUFFLE ->
|
||||||
context.getString(R.string.desc_shuffle),
|
PlaybackStateCompat.CustomAction.Builder(
|
||||||
if (playbackManager.isShuffled) {
|
PlaybackService.ACTION_INVERT_SHUFFLE,
|
||||||
R.drawable.ic_shuffle_on_24
|
context.getString(R.string.desc_shuffle),
|
||||||
} else {
|
if (playbackManager.isShuffled) {
|
||||||
R.drawable.ic_shuffle_off_24
|
R.drawable.ic_shuffle_on_24
|
||||||
}
|
} else {
|
||||||
)
|
R.drawable.ic_shuffle_off_24
|
||||||
|
})
|
||||||
else -> PlaybackStateCompat.CustomAction.Builder(
|
else ->
|
||||||
PlaybackService.ACTION_INC_REPEAT_MODE,
|
PlaybackStateCompat.CustomAction.Builder(
|
||||||
context.getString(R.string.desc_change_repeat),
|
PlaybackService.ACTION_INC_REPEAT_MODE,
|
||||||
playbackManager.repeatMode.icon
|
context.getString(R.string.desc_change_repeat),
|
||||||
)
|
playbackManager.repeatMode.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
val exitAction =
|
val exitAction =
|
||||||
PlaybackStateCompat.CustomAction.Builder(
|
PlaybackStateCompat.CustomAction.Builder(
|
||||||
PlaybackService.ACTION_EXIT,
|
PlaybackService.ACTION_EXIT,
|
||||||
context.getString(R.string.desc_exit),
|
context.getString(R.string.desc_exit),
|
||||||
R.drawable.ic_close_24
|
R.drawable.ic_close_24)
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
state.addCustomAction(extraAction.build())
|
state.addCustomAction(extraAction.build())
|
||||||
|
|
|
@ -52,12 +52,10 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
|
||||||
|
|
||||||
addAction(buildRepeatAction(context, RepeatMode.NONE))
|
addAction(buildRepeatAction(context, RepeatMode.NONE))
|
||||||
addAction(
|
addAction(
|
||||||
buildAction(context, PlaybackService.ACTION_SKIP_PREV, R.drawable.ic_skip_prev_24)
|
buildAction(context, PlaybackService.ACTION_SKIP_PREV, R.drawable.ic_skip_prev_24))
|
||||||
)
|
|
||||||
addAction(buildPlayPauseAction(context, true))
|
addAction(buildPlayPauseAction(context, true))
|
||||||
addAction(
|
addAction(
|
||||||
buildAction(context, PlaybackService.ACTION_SKIP_NEXT, R.drawable.ic_skip_next_24)
|
buildAction(context, PlaybackService.ACTION_SKIP_NEXT, R.drawable.ic_skip_next_24))
|
||||||
)
|
|
||||||
addAction(buildAction(context, PlaybackService.ACTION_EXIT, R.drawable.ic_close_24))
|
addAction(buildAction(context, PlaybackService.ACTION_EXIT, R.drawable.ic_close_24))
|
||||||
|
|
||||||
setStyle(MediaStyle().setMediaSession(sessionToken).setShowActionsInCompactView(1, 2, 3))
|
setStyle(MediaStyle().setMediaSession(sessionToken).setShowActionsInCompactView(1, 2, 3))
|
||||||
|
@ -134,10 +132,7 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
|
||||||
): NotificationCompat.Action {
|
): NotificationCompat.Action {
|
||||||
val action =
|
val action =
|
||||||
NotificationCompat.Action.Builder(
|
NotificationCompat.Action.Builder(
|
||||||
iconRes,
|
iconRes, actionName, context.newBroadcastPendingIntent(actionName))
|
||||||
actionName,
|
|
||||||
context.newBroadcastPendingIntent(actionName)
|
|
||||||
)
|
|
||||||
|
|
||||||
return action.build()
|
return action.build()
|
||||||
}
|
}
|
||||||
|
@ -146,7 +141,6 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
|
||||||
val CHANNEL_INFO =
|
val CHANNEL_INFO =
|
||||||
ChannelInfo(
|
ChannelInfo(
|
||||||
id = BuildConfig.APPLICATION_ID + ".channel.PLAYBACK",
|
id = BuildConfig.APPLICATION_ID + ".channel.PLAYBACK",
|
||||||
nameRes = R.string.lbl_playback
|
nameRes = R.string.lbl_playback)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,8 @@ import com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer
|
||||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
|
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector
|
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector
|
||||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
|
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
@ -57,8 +59,6 @@ import org.oxycblt.auxio.ui.system.ForegroundManager
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.widgets.WidgetComponent
|
import org.oxycblt.auxio.widgets.WidgetComponent
|
||||||
import org.oxycblt.auxio.widgets.WidgetProvider
|
import org.oxycblt.auxio.widgets.WidgetProvider
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service that manages the system-side aspects of playback, such as:
|
* A service that manages the system-side aspects of playback, such as:
|
||||||
|
@ -123,10 +123,8 @@ class PlaybackService :
|
||||||
handler,
|
handler,
|
||||||
audioListener,
|
audioListener,
|
||||||
AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES,
|
AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES,
|
||||||
replayGainProcessor
|
replayGainProcessor),
|
||||||
),
|
LibflacAudioRenderer(handler, audioListener, replayGainProcessor))
|
||||||
LibflacAudioRenderer(handler, audioListener, replayGainProcessor)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable constant bitrate seeking so that certain MP3s/AACs are seekable
|
// Enable constant bitrate seeking so that certain MP3s/AACs are seekable
|
||||||
|
@ -141,8 +139,7 @@ class PlaybackService :
|
||||||
.setUsage(C.USAGE_MEDIA)
|
.setUsage(C.USAGE_MEDIA)
|
||||||
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
|
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
|
||||||
.build(),
|
.build(),
|
||||||
true
|
true)
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
player.addListener(this)
|
player.addListener(this)
|
||||||
|
@ -227,10 +224,7 @@ class PlaybackService :
|
||||||
|
|
||||||
override fun makeState(durationMs: Long) =
|
override fun makeState(durationMs: Long) =
|
||||||
InternalPlayer.State.new(
|
InternalPlayer.State.new(
|
||||||
player.playWhenReady,
|
player.playWhenReady, player.isPlaying, max(min(player.currentPosition, durationMs), 0))
|
||||||
player.isPlaying,
|
|
||||||
max(min(player.currentPosition, durationMs), 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun loadSong(song: Song?, play: Boolean) {
|
override fun loadSong(song: Song?, play: Boolean) {
|
||||||
if (song == null) {
|
if (song == null) {
|
||||||
|
@ -340,8 +334,7 @@ class PlaybackService :
|
||||||
override fun onSettingChanged(key: String) {
|
override fun onSettingChanged(key: String) {
|
||||||
if (key == getString(R.string.set_key_replay_gain) ||
|
if (key == getString(R.string.set_key_replay_gain) ||
|
||||||
key == getString(R.string.set_key_pre_amp_with) ||
|
key == getString(R.string.set_key_pre_amp_with) ||
|
||||||
key == getString(R.string.set_key_pre_amp_without)
|
key == getString(R.string.set_key_pre_amp_without)) {
|
||||||
) {
|
|
||||||
onTracksChanged(player.currentTracks)
|
onTracksChanged(player.currentTracks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -353,8 +346,7 @@ class PlaybackService :
|
||||||
Intent(event)
|
Intent(event)
|
||||||
.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
|
.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
|
||||||
.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
|
.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
|
||||||
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
|
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stop the foreground state and hide the notification */
|
/** Stop the foreground state and hide the notification */
|
||||||
|
@ -378,9 +370,7 @@ class PlaybackService :
|
||||||
is InternalPlayer.Action.RestoreState -> {
|
is InternalPlayer.Action.RestoreState -> {
|
||||||
restoreScope.launch {
|
restoreScope.launch {
|
||||||
playbackManager.restoreState(
|
playbackManager.restoreState(
|
||||||
PlaybackStateDatabase.getInstance(this@PlaybackService),
|
PlaybackStateDatabase.getInstance(this@PlaybackService), false)
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is InternalPlayer.Action.ShuffleAll -> {
|
is InternalPlayer.Action.ShuffleAll -> {
|
||||||
|
@ -475,8 +465,7 @@ class PlaybackService :
|
||||||
private fun maybeResumeFromPlug() {
|
private fun maybeResumeFromPlug() {
|
||||||
if (playbackManager.song != null &&
|
if (playbackManager.song != null &&
|
||||||
settings.headsetAutoplay &&
|
settings.headsetAutoplay &&
|
||||||
initialHeadsetPlugEventHandled
|
initialHeadsetPlugEventHandled) {
|
||||||
) {
|
|
||||||
logD("Device connected, resuming")
|
logD("Device connected, resuming")
|
||||||
playbackManager.changePlaying(true)
|
playbackManager.changePlaying(true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,11 @@ package org.oxycblt.auxio.playback.ui
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import com.google.android.material.slider.Slider
|
import com.google.android.material.slider.Slider
|
||||||
|
import kotlin.math.max
|
||||||
import org.oxycblt.auxio.databinding.ViewSeekBarBinding
|
import org.oxycblt.auxio.databinding.ViewSeekBarBinding
|
||||||
import org.oxycblt.auxio.playback.formatDurationDs
|
import org.oxycblt.auxio.playback.formatDurationDs
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper around [Slider] that shows not only position and duration values, but also basically
|
* A wrapper around [Slider] that shows not only position and duration values, but also basically
|
||||||
|
|
|
@ -80,13 +80,14 @@ class SearchFragment :
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentSearchBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentSearchBinding, savedInstanceState: Bundle?) {
|
||||||
binding.searchToolbar.apply {
|
binding.searchToolbar.apply {
|
||||||
val itemIdToSelect = when (searchModel.filterMode) {
|
val itemIdToSelect =
|
||||||
MusicMode.SONGS -> R.id.option_filter_songs
|
when (searchModel.filterMode) {
|
||||||
MusicMode.ALBUMS -> R.id.option_filter_albums
|
MusicMode.SONGS -> R.id.option_filter_songs
|
||||||
MusicMode.ARTISTS -> R.id.option_filter_artists
|
MusicMode.ALBUMS -> R.id.option_filter_albums
|
||||||
MusicMode.GENRES -> R.id.option_filter_genres
|
MusicMode.ARTISTS -> R.id.option_filter_artists
|
||||||
null -> R.id.option_filter_all
|
MusicMode.GENRES -> R.id.option_filter_genres
|
||||||
}
|
null -> R.id.option_filter_all
|
||||||
|
}
|
||||||
|
|
||||||
menu.findItem(itemIdToSelect).isChecked = true
|
menu.findItem(itemIdToSelect).isChecked = true
|
||||||
|
|
||||||
|
@ -119,11 +120,7 @@ class SearchFragment :
|
||||||
|
|
||||||
collectImmediately(searchModel.searchResults, ::handleResults)
|
collectImmediately(searchModel.searchResults, ::handleResults)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song,
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
|
||||||
playbackModel.parent,
|
|
||||||
playbackModel.isPlaying,
|
|
||||||
::handlePlayback
|
|
||||||
)
|
|
||||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,12 +145,13 @@ class SearchFragment :
|
||||||
|
|
||||||
override fun onItemClick(item: Item) {
|
override fun onItemClick(item: Item) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> when (settings.libPlaybackMode) {
|
is Song ->
|
||||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
when (settings.libPlaybackMode) {
|
||||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||||
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
|
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||||
else -> error("Unexpected playback mode: ${settings.libPlaybackMode}")
|
MusicMode.ARTISTS -> doArtistDependentAction(item, PickerMode.PLAY)
|
||||||
}
|
else -> error("Unexpected playback mode: ${settings.libPlaybackMode}")
|
||||||
|
}
|
||||||
is MusicParent -> navModel.exploreNavigateTo(item)
|
is MusicParent -> navModel.exploreNavigateTo(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -194,8 +192,7 @@ class SearchFragment :
|
||||||
is Artist -> SearchFragmentDirections.actionShowArtist(item.uid)
|
is Artist -> SearchFragmentDirections.actionShowArtist(item.uid)
|
||||||
is Genre -> SearchFragmentDirections.actionShowGenre(item.uid)
|
is Genre -> SearchFragmentDirections.actionShowGenre(item.uid)
|
||||||
else -> return
|
else -> return
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
imm.hide()
|
imm.hide()
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import androidx.annotation.IdRes
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import java.text.Normalizer
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
@ -42,7 +43,6 @@ import org.oxycblt.auxio.ui.recycler.Header
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.util.application
|
import org.oxycblt.auxio.util.application
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import java.text.Normalizer
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [ViewModel] for search functionality.
|
* The [ViewModel] for search functionality.
|
||||||
|
@ -87,43 +87,44 @@ class SearchViewModel(application: Application) :
|
||||||
logD("Performing search for $query")
|
logD("Performing search for $query")
|
||||||
|
|
||||||
// Searching can be quite expensive, so get on a co-routine
|
// Searching can be quite expensive, so get on a co-routine
|
||||||
currentSearchJob = viewModelScope.launch {
|
currentSearchJob =
|
||||||
val sort = Sort(Sort.Mode.ByName, true)
|
viewModelScope.launch {
|
||||||
val results = mutableListOf<Item>()
|
val sort = Sort(Sort.Mode.ByName, true)
|
||||||
|
val results = mutableListOf<Item>()
|
||||||
|
|
||||||
// Note: a filter mode of null means to not filter at all.
|
// Note: a filter mode of null means to not filter at all.
|
||||||
|
|
||||||
if (filterMode == null || filterMode == MusicMode.ARTISTS) {
|
if (filterMode == null || filterMode == MusicMode.ARTISTS) {
|
||||||
library.artists.filterArtistsBy(query)?.let { artists ->
|
library.artists.filterArtistsBy(query)?.let { artists ->
|
||||||
results.add(Header(R.string.lbl_artists))
|
results.add(Header(R.string.lbl_artists))
|
||||||
results.addAll(sort.artists(artists))
|
results.addAll(sort.artists(artists))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (filterMode == null || filterMode == MusicMode.ALBUMS) {
|
if (filterMode == null || filterMode == MusicMode.ALBUMS) {
|
||||||
library.albums.filterAlbumsBy(query)?.let { albums ->
|
library.albums.filterAlbumsBy(query)?.let { albums ->
|
||||||
results.add(Header(R.string.lbl_albums))
|
results.add(Header(R.string.lbl_albums))
|
||||||
results.addAll(sort.albums(albums))
|
results.addAll(sort.albums(albums))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (filterMode == null || filterMode == MusicMode.GENRES) {
|
if (filterMode == null || filterMode == MusicMode.GENRES) {
|
||||||
library.genres.filterGenresBy(query)?.let { genres ->
|
library.genres.filterGenresBy(query)?.let { genres ->
|
||||||
results.add(Header(R.string.lbl_genres))
|
results.add(Header(R.string.lbl_genres))
|
||||||
results.addAll(sort.genres(genres))
|
results.addAll(sort.genres(genres))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (filterMode == null || filterMode == MusicMode.SONGS) {
|
if (filterMode == null || filterMode == MusicMode.SONGS) {
|
||||||
library.songs.filterSongsBy(query)?.let { songs ->
|
library.songs.filterSongsBy(query)?.let { songs ->
|
||||||
results.add(Header(R.string.lbl_songs))
|
results.add(Header(R.string.lbl_songs))
|
||||||
results.addAll(sort.songs(songs))
|
results.addAll(sort.songs(songs))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
yield()
|
yield()
|
||||||
_searchResults.value = results
|
_searchResults.value = results
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -162,14 +163,14 @@ class SearchViewModel(application: Application) :
|
||||||
|
|
||||||
private inline fun <T : Music> List<T>.baseFilterBy(value: String, fallback: (T) -> Boolean) =
|
private inline fun <T : Music> List<T>.baseFilterBy(value: String, fallback: (T) -> Boolean) =
|
||||||
filter {
|
filter {
|
||||||
// The basic comparison is first by the *normalized* name, as that allows a
|
// The basic comparison is first by the *normalized* name, as that allows a
|
||||||
// non-unicode search to match with some unicode characters. In an ideal world, we
|
// non-unicode search to match with some unicode characters. In an ideal world, we
|
||||||
// would just want to leverage CollationKey, but that is not designed for a contains
|
// would just want to leverage CollationKey, but that is not designed for a contains
|
||||||
// algorithm. If that fails, filter impls have fallback values, primarily around
|
// algorithm. If that fails, filter impls have fallback values, primarily around
|
||||||
// sort tags or file names.
|
// sort tags or file names.
|
||||||
it.resolveNameNormalized(application).contains(value, ignoreCase = true) ||
|
it.resolveNameNormalized(application).contains(value, ignoreCase = true) ||
|
||||||
fallback(it)
|
fallback(it)
|
||||||
}
|
}
|
||||||
.ifEmpty { null }
|
.ifEmpty { null }
|
||||||
|
|
||||||
private fun Music.resolveNameNormalized(context: Context): String {
|
private fun Music.resolveNameNormalized(context: Context): String {
|
||||||
|
|
|
@ -84,8 +84,7 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
|
||||||
binding.aboutTotalDuration.text =
|
binding.aboutTotalDuration.text =
|
||||||
getString(
|
getString(
|
||||||
R.string.fmt_lib_total_duration,
|
R.string.fmt_lib_total_duration,
|
||||||
songs.sumOf { it.durationMs }.formatDurationMs(false)
|
songs.sumOf { it.durationMs }.formatDurationMs(false))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateAlbumCount(albums: List<Album>) {
|
private fun updateAlbumCount(albums: List<Album>) {
|
||||||
|
|
|
@ -84,11 +84,12 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
if (inner.contains(OldKeys.KEY_SHOW_COVERS) || inner.contains(OldKeys.KEY_QUALITY_COVERS)) {
|
if (inner.contains(OldKeys.KEY_SHOW_COVERS) || inner.contains(OldKeys.KEY_QUALITY_COVERS)) {
|
||||||
logD("Migrating cover settings")
|
logD("Migrating cover settings")
|
||||||
|
|
||||||
val mode = when {
|
val mode =
|
||||||
!inner.getBoolean(OldKeys.KEY_SHOW_COVERS, true) -> CoverMode.OFF
|
when {
|
||||||
!inner.getBoolean(OldKeys.KEY_QUALITY_COVERS, true) -> CoverMode.MEDIA_STORE
|
!inner.getBoolean(OldKeys.KEY_SHOW_COVERS, true) -> CoverMode.OFF
|
||||||
else -> CoverMode.QUALITY
|
!inner.getBoolean(OldKeys.KEY_QUALITY_COVERS, true) -> CoverMode.MEDIA_STORE
|
||||||
}
|
else -> CoverMode.QUALITY
|
||||||
|
}
|
||||||
|
|
||||||
inner.edit {
|
inner.edit {
|
||||||
putInt(context.getString(R.string.set_key_cover_mode), mode.intCode)
|
putInt(context.getString(R.string.set_key_cover_mode), mode.intCode)
|
||||||
|
@ -100,11 +101,12 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
if (inner.contains(OldKeys.KEY_ALT_NOTIF_ACTION)) {
|
if (inner.contains(OldKeys.KEY_ALT_NOTIF_ACTION)) {
|
||||||
logD("Migrating ${OldKeys.KEY_ALT_NOTIF_ACTION}")
|
logD("Migrating ${OldKeys.KEY_ALT_NOTIF_ACTION}")
|
||||||
|
|
||||||
val mode = if (inner.getBoolean(OldKeys.KEY_ALT_NOTIF_ACTION, false)) {
|
val mode =
|
||||||
ActionMode.SHUFFLE
|
if (inner.getBoolean(OldKeys.KEY_ALT_NOTIF_ACTION, false)) {
|
||||||
} else {
|
ActionMode.SHUFFLE
|
||||||
ActionMode.REPEAT
|
} else {
|
||||||
}
|
ActionMode.REPEAT
|
||||||
|
}
|
||||||
|
|
||||||
inner.edit {
|
inner.edit {
|
||||||
putInt(context.getString(R.string.set_key_notif_action), mode.intCode)
|
putInt(context.getString(R.string.set_key_notif_action), mode.intCode)
|
||||||
|
@ -125,10 +127,11 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
if (inner.contains(OldKeys.KEY_LIB_PLAYBACK_MODE)) {
|
if (inner.contains(OldKeys.KEY_LIB_PLAYBACK_MODE)) {
|
||||||
logD("Migrating ${OldKeys.KEY_LIB_PLAYBACK_MODE}")
|
logD("Migrating ${OldKeys.KEY_LIB_PLAYBACK_MODE}")
|
||||||
|
|
||||||
val mode = inner.getInt(
|
val mode =
|
||||||
OldKeys.KEY_LIB_PLAYBACK_MODE,
|
inner
|
||||||
IntegerTable.PLAYBACK_MODE_ALL_SONGS
|
.getInt(OldKeys.KEY_LIB_PLAYBACK_MODE, IntegerTable.PLAYBACK_MODE_ALL_SONGS)
|
||||||
).migratePlaybackMode() ?: MusicMode.SONGS
|
.migratePlaybackMode()
|
||||||
|
?: MusicMode.SONGS
|
||||||
|
|
||||||
inner.edit {
|
inner.edit {
|
||||||
putInt(context.getString(R.string.set_key_library_song_playback_mode), mode.intCode)
|
putInt(context.getString(R.string.set_key_library_song_playback_mode), mode.intCode)
|
||||||
|
@ -140,13 +143,13 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
if (inner.contains(OldKeys.KEY_DETAIL_PLAYBACK_MODE)) {
|
if (inner.contains(OldKeys.KEY_DETAIL_PLAYBACK_MODE)) {
|
||||||
logD("Migrating ${OldKeys.KEY_DETAIL_PLAYBACK_MODE}")
|
logD("Migrating ${OldKeys.KEY_DETAIL_PLAYBACK_MODE}")
|
||||||
|
|
||||||
val mode = inner.getInt(OldKeys.KEY_DETAIL_PLAYBACK_MODE, Int.MIN_VALUE).migratePlaybackMode()
|
val mode =
|
||||||
|
inner.getInt(OldKeys.KEY_DETAIL_PLAYBACK_MODE, Int.MIN_VALUE).migratePlaybackMode()
|
||||||
|
|
||||||
inner.edit {
|
inner.edit {
|
||||||
putInt(
|
putInt(
|
||||||
context.getString(R.string.set_key_detail_song_playback_mode),
|
context.getString(R.string.set_key_detail_song_playback_mode),
|
||||||
mode?.intCode ?: Int.MIN_VALUE
|
mode?.intCode ?: Int.MIN_VALUE)
|
||||||
)
|
|
||||||
remove(OldKeys.KEY_DETAIL_PLAYBACK_MODE)
|
remove(OldKeys.KEY_DETAIL_PLAYBACK_MODE)
|
||||||
apply()
|
apply()
|
||||||
}
|
}
|
||||||
|
@ -173,8 +176,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
get() =
|
get() =
|
||||||
inner.getInt(
|
inner.getInt(
|
||||||
context.getString(R.string.set_key_theme),
|
context.getString(R.string.set_key_theme),
|
||||||
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||||
)
|
|
||||||
|
|
||||||
/** Whether the dark theme should be black or not */
|
/** Whether the dark theme should be black or not */
|
||||||
val useBlackTheme: Boolean
|
val useBlackTheme: Boolean
|
||||||
|
@ -182,7 +184,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
|
|
||||||
/** The current accent. */
|
/** The current accent. */
|
||||||
var accent: Accent
|
var accent: Accent
|
||||||
get() = Accent.from(inner.getInt(context.getString(R.string.set_key_accent), Accent.DEFAULT))
|
get() =
|
||||||
|
Accent.from(inner.getInt(context.getString(R.string.set_key_accent), Accent.DEFAULT))
|
||||||
set(value) {
|
set(value) {
|
||||||
inner.edit {
|
inner.edit {
|
||||||
putInt(context.getString(R.string.set_key_accent), value.index)
|
putInt(context.getString(R.string.set_key_accent), value.index)
|
||||||
|
@ -194,8 +197,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
var libTabs: Array<Tab>
|
var libTabs: Array<Tab>
|
||||||
get() =
|
get() =
|
||||||
Tab.fromSequence(
|
Tab.fromSequence(
|
||||||
inner.getInt(context.getString(R.string.set_key_lib_tabs), Tab.SEQUENCE_DEFAULT)
|
inner.getInt(context.getString(R.string.set_key_lib_tabs), Tab.SEQUENCE_DEFAULT))
|
||||||
)
|
|
||||||
?: unlikelyToBeNull(Tab.fromSequence(Tab.SEQUENCE_DEFAULT))
|
?: unlikelyToBeNull(Tab.fromSequence(Tab.SEQUENCE_DEFAULT))
|
||||||
set(value) {
|
set(value) {
|
||||||
inner.edit {
|
inner.edit {
|
||||||
|
@ -216,15 +218,15 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
val actionMode: ActionMode
|
val actionMode: ActionMode
|
||||||
get() =
|
get() =
|
||||||
ActionMode.fromIntCode(
|
ActionMode.fromIntCode(
|
||||||
inner.getInt(context.getString(R.string.set_key_bar_action), Int.MIN_VALUE)
|
inner.getInt(context.getString(R.string.set_key_bar_action), Int.MIN_VALUE))
|
||||||
)
|
|
||||||
?: ActionMode.NEXT
|
?: ActionMode.NEXT
|
||||||
|
|
||||||
/**
|
/** The custom action to display in the notification. */
|
||||||
* The custom action to display in the notification.
|
|
||||||
*/
|
|
||||||
val notifAction: ActionMode
|
val notifAction: ActionMode
|
||||||
get() = ActionMode.fromIntCode(inner.getInt(context.getString(R.string.set_key_notif_action), Int.MIN_VALUE)) ?: ActionMode.REPEAT
|
get() =
|
||||||
|
ActionMode.fromIntCode(
|
||||||
|
inner.getInt(context.getString(R.string.set_key_notif_action), Int.MIN_VALUE))
|
||||||
|
?: ActionMode.REPEAT
|
||||||
|
|
||||||
/** Whether to resume playback when a headset is connected (may not work well in all cases) */
|
/** Whether to resume playback when a headset is connected (may not work well in all cases) */
|
||||||
val headsetAutoplay: Boolean
|
val headsetAutoplay: Boolean
|
||||||
|
@ -234,8 +236,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
val replayGainMode: ReplayGainMode
|
val replayGainMode: ReplayGainMode
|
||||||
get() =
|
get() =
|
||||||
ReplayGainMode.fromIntCode(
|
ReplayGainMode.fromIntCode(
|
||||||
inner.getInt(context.getString(R.string.set_key_replay_gain), Int.MIN_VALUE)
|
inner.getInt(context.getString(R.string.set_key_replay_gain), Int.MIN_VALUE))
|
||||||
)
|
|
||||||
?: ReplayGainMode.DYNAMIC
|
?: ReplayGainMode.DYNAMIC
|
||||||
|
|
||||||
/** The current ReplayGain pre-amp configuration */
|
/** The current ReplayGain pre-amp configuration */
|
||||||
|
@ -243,8 +244,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
get() =
|
get() =
|
||||||
ReplayGainPreAmp(
|
ReplayGainPreAmp(
|
||||||
inner.getFloat(context.getString(R.string.set_key_pre_amp_with), 0f),
|
inner.getFloat(context.getString(R.string.set_key_pre_amp_with), 0f),
|
||||||
inner.getFloat(context.getString(R.string.set_key_pre_amp_without), 0f)
|
inner.getFloat(context.getString(R.string.set_key_pre_amp_without), 0f))
|
||||||
)
|
|
||||||
set(value) {
|
set(value) {
|
||||||
inner.edit {
|
inner.edit {
|
||||||
putFloat(context.getString(R.string.set_key_pre_amp_with), value.with)
|
putFloat(context.getString(R.string.set_key_pre_amp_with), value.with)
|
||||||
|
@ -258,10 +258,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
get() =
|
get() =
|
||||||
MusicMode.fromInt(
|
MusicMode.fromInt(
|
||||||
inner.getInt(
|
inner.getInt(
|
||||||
context.getString(R.string.set_key_library_song_playback_mode),
|
context.getString(R.string.set_key_library_song_playback_mode), Int.MIN_VALUE))
|
||||||
Int.MIN_VALUE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
?: MusicMode.SONGS
|
?: MusicMode.SONGS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -272,10 +269,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
get() =
|
get() =
|
||||||
MusicMode.fromInt(
|
MusicMode.fromInt(
|
||||||
inner.getInt(
|
inner.getInt(
|
||||||
context.getString(R.string.set_key_detail_song_playback_mode),
|
context.getString(R.string.set_key_detail_song_playback_mode), Int.MIN_VALUE))
|
||||||
Int.MIN_VALUE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Whether shuffle should stay on when a new song is selected. */
|
/** Whether shuffle should stay on when a new song is selected. */
|
||||||
val keepShuffle: Boolean
|
val keepShuffle: Boolean
|
||||||
|
@ -298,7 +292,10 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
|
|
||||||
/** The strategy used when loading images. */
|
/** The strategy used when loading images. */
|
||||||
val coverMode: CoverMode
|
val coverMode: CoverMode
|
||||||
get() = CoverMode.fromIntCode(inner.getInt(context.getString(R.string.set_key_cover_mode), Int.MIN_VALUE)) ?: CoverMode.MEDIA_STORE
|
get() =
|
||||||
|
CoverMode.fromIntCode(
|
||||||
|
inner.getInt(context.getString(R.string.set_key_cover_mode), Int.MIN_VALUE))
|
||||||
|
?: CoverMode.MEDIA_STORE
|
||||||
|
|
||||||
/** Whether to load all audio files, even ones not considered music. */
|
/** Whether to load all audio files, even ones not considered music. */
|
||||||
val excludeNonMusic: Boolean
|
val excludeNonMusic: Boolean
|
||||||
|
@ -311,9 +308,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
.mapNotNull { Directory.fromDocumentUri(storageManager, it) }
|
.mapNotNull { Directory.fromDocumentUri(storageManager, it) }
|
||||||
|
|
||||||
return MusicDirs(
|
return MusicDirs(
|
||||||
dirs,
|
dirs, inner.getBoolean(context.getString(R.string.set_key_music_dirs_include), false))
|
||||||
inner.getBoolean(context.getString(R.string.set_key_music_dirs_include), false)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Set the list of directories that music should be hidden/loaded from. */
|
/** Set the list of directories that music should be hidden/loaded from. */
|
||||||
|
@ -321,12 +316,9 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
inner.edit {
|
inner.edit {
|
||||||
putStringSet(
|
putStringSet(
|
||||||
context.getString(R.string.set_key_music_dirs),
|
context.getString(R.string.set_key_music_dirs),
|
||||||
musicDirs.dirs.map(Directory::toDocumentUri).toSet()
|
musicDirs.dirs.map(Directory::toDocumentUri).toSet())
|
||||||
)
|
|
||||||
putBoolean(
|
putBoolean(
|
||||||
context.getString(R.string.set_key_music_dirs_include),
|
context.getString(R.string.set_key_music_dirs_include), musicDirs.shouldInclude)
|
||||||
musicDirs.shouldInclude
|
|
||||||
)
|
|
||||||
apply()
|
apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -348,14 +340,12 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
var searchFilterMode: MusicMode?
|
var searchFilterMode: MusicMode?
|
||||||
get() =
|
get() =
|
||||||
MusicMode.fromInt(
|
MusicMode.fromInt(
|
||||||
inner.getInt(context.getString(R.string.set_key_search_filter), Int.MIN_VALUE)
|
inner.getInt(context.getString(R.string.set_key_search_filter), Int.MIN_VALUE))
|
||||||
)
|
|
||||||
set(value) {
|
set(value) {
|
||||||
inner.edit {
|
inner.edit {
|
||||||
putInt(
|
putInt(
|
||||||
context.getString(R.string.set_key_search_filter),
|
context.getString(R.string.set_key_search_filter),
|
||||||
value?.intCode ?: Int.MIN_VALUE
|
value?.intCode ?: Int.MIN_VALUE)
|
||||||
)
|
|
||||||
apply()
|
apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -364,8 +354,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
var libSongSort: Sort
|
var libSongSort: Sort
|
||||||
get() =
|
get() =
|
||||||
Sort.fromIntCode(
|
Sort.fromIntCode(
|
||||||
inner.getInt(context.getString(R.string.set_key_lib_songs_sort), Int.MIN_VALUE)
|
inner.getInt(context.getString(R.string.set_key_lib_songs_sort), Int.MIN_VALUE))
|
||||||
)
|
|
||||||
?: Sort(Sort.Mode.ByName, true)
|
?: Sort(Sort.Mode.ByName, true)
|
||||||
set(value) {
|
set(value) {
|
||||||
inner.edit {
|
inner.edit {
|
||||||
|
@ -378,8 +367,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
var libAlbumSort: Sort
|
var libAlbumSort: Sort
|
||||||
get() =
|
get() =
|
||||||
Sort.fromIntCode(
|
Sort.fromIntCode(
|
||||||
inner.getInt(context.getString(R.string.set_key_lib_albums_sort), Int.MIN_VALUE)
|
inner.getInt(context.getString(R.string.set_key_lib_albums_sort), Int.MIN_VALUE))
|
||||||
)
|
|
||||||
?: Sort(Sort.Mode.ByName, true)
|
?: Sort(Sort.Mode.ByName, true)
|
||||||
set(value) {
|
set(value) {
|
||||||
inner.edit {
|
inner.edit {
|
||||||
|
@ -392,8 +380,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
var libArtistSort: Sort
|
var libArtistSort: Sort
|
||||||
get() =
|
get() =
|
||||||
Sort.fromIntCode(
|
Sort.fromIntCode(
|
||||||
inner.getInt(context.getString(R.string.set_key_lib_artists_sort), Int.MIN_VALUE)
|
inner.getInt(context.getString(R.string.set_key_lib_artists_sort), Int.MIN_VALUE))
|
||||||
)
|
|
||||||
?: Sort(Sort.Mode.ByName, true)
|
?: Sort(Sort.Mode.ByName, true)
|
||||||
set(value) {
|
set(value) {
|
||||||
inner.edit {
|
inner.edit {
|
||||||
|
@ -406,8 +393,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
var libGenreSort: Sort
|
var libGenreSort: Sort
|
||||||
get() =
|
get() =
|
||||||
Sort.fromIntCode(
|
Sort.fromIntCode(
|
||||||
inner.getInt(context.getString(R.string.set_key_lib_genres_sort), Int.MIN_VALUE)
|
inner.getInt(context.getString(R.string.set_key_lib_genres_sort), Int.MIN_VALUE))
|
||||||
)
|
|
||||||
?: Sort(Sort.Mode.ByName, true)
|
?: Sort(Sort.Mode.ByName, true)
|
||||||
set(value) {
|
set(value) {
|
||||||
inner.edit {
|
inner.edit {
|
||||||
|
@ -422,10 +408,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
var sort =
|
var sort =
|
||||||
Sort.fromIntCode(
|
Sort.fromIntCode(
|
||||||
inner.getInt(
|
inner.getInt(
|
||||||
context.getString(R.string.set_key_detail_album_sort),
|
context.getString(R.string.set_key_detail_album_sort), Int.MIN_VALUE))
|
||||||
Int.MIN_VALUE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
?: Sort(Sort.Mode.ByDisc, true)
|
?: Sort(Sort.Mode.ByDisc, true)
|
||||||
|
|
||||||
// Correct legacy album sort modes to Disc
|
// Correct legacy album sort modes to Disc
|
||||||
|
@ -446,8 +429,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
var detailArtistSort: Sort
|
var detailArtistSort: Sort
|
||||||
get() =
|
get() =
|
||||||
Sort.fromIntCode(
|
Sort.fromIntCode(
|
||||||
inner.getInt(context.getString(R.string.set_key_detail_artist_sort), Int.MIN_VALUE)
|
inner.getInt(context.getString(R.string.set_key_detail_artist_sort), Int.MIN_VALUE))
|
||||||
)
|
|
||||||
?: Sort(Sort.Mode.ByDate, false)
|
?: Sort(Sort.Mode.ByDate, false)
|
||||||
set(value) {
|
set(value) {
|
||||||
inner.edit {
|
inner.edit {
|
||||||
|
@ -460,8 +442,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
var detailGenreSort: Sort
|
var detailGenreSort: Sort
|
||||||
get() =
|
get() =
|
||||||
Sort.fromIntCode(
|
Sort.fromIntCode(
|
||||||
inner.getInt(context.getString(R.string.set_key_detail_genre_sort), Int.MIN_VALUE)
|
inner.getInt(context.getString(R.string.set_key_detail_genre_sort), Int.MIN_VALUE))
|
||||||
)
|
|
||||||
?: Sort(Sort.Mode.ByName, true)
|
?: Sort(Sort.Mode.ByName, true)
|
||||||
set(value) {
|
set(value) {
|
||||||
inner.edit {
|
inner.edit {
|
||||||
|
|
|
@ -26,10 +26,10 @@ import androidx.core.content.res.getTextArrayOrThrow
|
||||||
import androidx.preference.DialogPreference
|
import androidx.preference.DialogPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceViewHolder
|
import androidx.preference.PreferenceViewHolder
|
||||||
|
import java.lang.reflect.Field
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.util.lazyReflectedField
|
import org.oxycblt.auxio.util.lazyReflectedField
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import java.lang.reflect.Field
|
|
||||||
|
|
||||||
class IntListPreference
|
class IntListPreference
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
|
@ -57,18 +57,13 @@ constructor(
|
||||||
init {
|
init {
|
||||||
val prefAttrs =
|
val prefAttrs =
|
||||||
context.obtainStyledAttributes(
|
context.obtainStyledAttributes(
|
||||||
attrs,
|
attrs, R.styleable.IntListPreference, defStyleAttr, defStyleRes)
|
||||||
R.styleable.IntListPreference,
|
|
||||||
defStyleAttr,
|
|
||||||
defStyleRes
|
|
||||||
)
|
|
||||||
|
|
||||||
entries = prefAttrs.getTextArrayOrThrow(R.styleable.IntListPreference_entries)
|
entries = prefAttrs.getTextArrayOrThrow(R.styleable.IntListPreference_entries)
|
||||||
|
|
||||||
values =
|
values =
|
||||||
context.resources.getIntArray(
|
context.resources.getIntArray(
|
||||||
prefAttrs.getResourceIdOrThrow(R.styleable.IntListPreference_entryValues)
|
prefAttrs.getResourceIdOrThrow(R.styleable.IntListPreference_entryValues))
|
||||||
)
|
|
||||||
|
|
||||||
val offValueId = prefAttrs.getResourceId(R.styleable.IntListPreference_offValue, -1)
|
val offValueId = prefAttrs.getResourceId(R.styleable.IntListPreference_offValue, -1)
|
||||||
if (offValueId > -1) {
|
if (offValueId > -1) {
|
||||||
|
@ -152,6 +147,6 @@ constructor(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val PREFERENCE_DEFAULT_VALUE_FIELD: Field by
|
private val PREFERENCE_DEFAULT_VALUE_FIELD: Field by
|
||||||
lazyReflectedField(Preference::class, "mDefaultValue")
|
lazyReflectedField(Preference::class, "mDefaultValue")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,6 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.settings.prefs
|
package org.oxycblt.auxio.settings.prefs
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
@ -43,8 +41,6 @@ import org.oxycblt.auxio.util.isNight
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
import java.security.Permission
|
|
||||||
import java.util.jar.Manifest
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The actual fragment containing the settings menu. Inherits [PreferenceFragmentCompat].
|
* The actual fragment containing the settings menu. Inherits [PreferenceFragmentCompat].
|
||||||
|
@ -98,14 +94,20 @@ class PreferenceFragment : PreferenceFragmentCompat() {
|
||||||
}
|
}
|
||||||
is WrappedDialogPreference -> {
|
is WrappedDialogPreference -> {
|
||||||
val context = requireContext()
|
val context = requireContext()
|
||||||
val directions = when (preference.key) {
|
val directions =
|
||||||
context.getString(R.string.set_key_accent) -> SettingsFragmentDirections.goToAccentDialog()
|
when (preference.key) {
|
||||||
context.getString(R.string.set_key_lib_tabs) -> SettingsFragmentDirections.goToTabDialog()
|
context.getString(R.string.set_key_accent) ->
|
||||||
context.getString(R.string.set_key_pre_amp) -> SettingsFragmentDirections.goToPreAmpDialog()
|
SettingsFragmentDirections.goToAccentDialog()
|
||||||
context.getString(R.string.set_key_music_dirs) -> SettingsFragmentDirections.goToMusicDirsDialog()
|
context.getString(R.string.set_key_lib_tabs) ->
|
||||||
getString(R.string.set_key_separators) -> SettingsFragmentDirections.goToSeparatorsDialog()
|
SettingsFragmentDirections.goToTabDialog()
|
||||||
else -> error("Unexpected dialog key ${preference.key}")
|
context.getString(R.string.set_key_pre_amp) ->
|
||||||
}
|
SettingsFragmentDirections.goToPreAmpDialog()
|
||||||
|
context.getString(R.string.set_key_music_dirs) ->
|
||||||
|
SettingsFragmentDirections.goToMusicDirsDialog()
|
||||||
|
getString(R.string.set_key_separators) ->
|
||||||
|
SettingsFragmentDirections.goToSeparatorsDialog()
|
||||||
|
else -> error("Unexpected dialog key ${preference.key}")
|
||||||
|
}
|
||||||
findNavController().navigate(directions)
|
findNavController().navigate(directions)
|
||||||
}
|
}
|
||||||
else -> super.onDisplayPreferenceDialog(preference)
|
else -> super.onDisplayPreferenceDialog(preference)
|
||||||
|
|
|
@ -49,14 +49,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
if (child != null) {
|
if (child != null) {
|
||||||
val coordinator = parent as CoordinatorLayout
|
val coordinator = parent as CoordinatorLayout
|
||||||
coordinatorLayoutBehavior?.onNestedPreScroll(
|
coordinatorLayoutBehavior?.onNestedPreScroll(
|
||||||
coordinator,
|
coordinator, this, coordinator, 0, 0, tConsumed, 0)
|
||||||
this,
|
|
||||||
coordinator,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
tConsumed,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|
|
@ -23,10 +23,10 @@ import android.view.View
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import com.google.android.material.bottomsheet.NeoBottomSheetBehavior
|
import com.google.android.material.bottomsheet.NeoBottomSheetBehavior
|
||||||
|
import kotlin.math.abs
|
||||||
import org.oxycblt.auxio.util.coordinatorLayoutBehavior
|
import org.oxycblt.auxio.util.coordinatorLayoutBehavior
|
||||||
import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
|
import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A behavior that automatically re-layouts and re-insets content to align with the parent layout's
|
* A behavior that automatically re-layouts and re-insets content to align with the parent layout's
|
||||||
|
@ -127,11 +127,7 @@ class BottomSheetContentBehavior<V : View>(context: Context, attributeSet: Attri
|
||||||
val bars = insets.systemBarInsetsCompat
|
val bars = insets.systemBarInsetsCompat
|
||||||
|
|
||||||
insets.replaceSystemBarInsetsCompat(
|
insets.replaceSystemBarInsetsCompat(
|
||||||
bars.left,
|
bars.left, bars.top, bars.right, (bars.bottom - consumed).coerceAtLeast(0))
|
||||||
bars.top,
|
|
||||||
bars.right,
|
|
||||||
(bars.bottom - consumed).coerceAtLeast(0)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setup = true
|
setup = true
|
||||||
|
@ -145,9 +141,7 @@ class BottomSheetContentBehavior<V : View>(context: Context, attributeSet: Attri
|
||||||
View.MeasureSpec.makeMeasureSpec(parent.measuredWidth, View.MeasureSpec.EXACTLY)
|
View.MeasureSpec.makeMeasureSpec(parent.measuredWidth, View.MeasureSpec.EXACTLY)
|
||||||
val contentHeightSpec =
|
val contentHeightSpec =
|
||||||
View.MeasureSpec.makeMeasureSpec(
|
View.MeasureSpec.makeMeasureSpec(
|
||||||
parent.measuredHeight - consumed,
|
parent.measuredHeight - consumed, View.MeasureSpec.EXACTLY)
|
||||||
View.MeasureSpec.EXACTLY
|
|
||||||
)
|
|
||||||
|
|
||||||
child.measure(contentWidthSpec, contentHeightSpec)
|
child.measure(contentWidthSpec, contentHeightSpec)
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,8 +39,7 @@ private val ACCENT_NAMES =
|
||||||
R.string.clr_orange,
|
R.string.clr_orange,
|
||||||
R.string.clr_brown,
|
R.string.clr_brown,
|
||||||
R.string.clr_grey,
|
R.string.clr_grey,
|
||||||
R.string.clr_dynamic
|
R.string.clr_dynamic)
|
||||||
)
|
|
||||||
|
|
||||||
private val ACCENT_THEMES =
|
private val ACCENT_THEMES =
|
||||||
intArrayOf(
|
intArrayOf(
|
||||||
|
@ -61,7 +60,7 @@ private val ACCENT_THEMES =
|
||||||
R.style.Theme_Auxio_Brown,
|
R.style.Theme_Auxio_Brown,
|
||||||
R.style.Theme_Auxio_Grey,
|
R.style.Theme_Auxio_Grey,
|
||||||
R.style.Theme_Auxio_App // Dynamic colors are on the base theme
|
R.style.Theme_Auxio_App // Dynamic colors are on the base theme
|
||||||
)
|
)
|
||||||
|
|
||||||
private val ACCENT_BLACK_THEMES =
|
private val ACCENT_BLACK_THEMES =
|
||||||
intArrayOf(
|
intArrayOf(
|
||||||
|
@ -82,7 +81,7 @@ private val ACCENT_BLACK_THEMES =
|
||||||
R.style.Theme_Auxio_Black_Brown,
|
R.style.Theme_Auxio_Black_Brown,
|
||||||
R.style.Theme_Auxio_Black_Grey,
|
R.style.Theme_Auxio_Black_Grey,
|
||||||
R.style.Theme_Auxio_Black // Dynamic colors are on the base theme
|
R.style.Theme_Auxio_Black // Dynamic colors are on the base theme
|
||||||
)
|
)
|
||||||
|
|
||||||
private val ACCENT_PRIMARY_COLORS =
|
private val ACCENT_PRIMARY_COLORS =
|
||||||
intArrayOf(
|
intArrayOf(
|
||||||
|
@ -102,8 +101,7 @@ private val ACCENT_PRIMARY_COLORS =
|
||||||
R.color.orange_primary,
|
R.color.orange_primary,
|
||||||
R.color.brown_primary,
|
R.color.brown_primary,
|
||||||
R.color.grey_primary,
|
R.color.grey_primary,
|
||||||
R.color.dynamic_primary
|
R.color.dynamic_primary)
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The data object for an accent. In the UI this is known as a "Color Scheme." This can be nominally
|
* The data object for an accent. In the UI this is known as a "Color Scheme." This can be nominally
|
||||||
|
|
|
@ -62,8 +62,7 @@ class AccentCustomizeDialog :
|
||||||
Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT))
|
Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT))
|
||||||
} else {
|
} else {
|
||||||
settings.accent
|
settings.accent
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
|
|
@ -21,9 +21,9 @@ import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlin.math.max
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.util.getDimenSize
|
import org.oxycblt.auxio.util.getDimenSize
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A sub-class of [GridLayoutManager] that automatically sets the spans so that they fit the width
|
* A sub-class of [GridLayoutManager] that automatically sets the spans so that they fit the width
|
||||||
|
|
|
@ -165,8 +165,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0)
|
||||||
centerY + radius,
|
centerY + radius,
|
||||||
startAngle,
|
startAngle,
|
||||||
sweepAngle,
|
sweepAngle,
|
||||||
false
|
false)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ import androidx.core.view.isInvisible
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlin.math.abs
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
|
import org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
|
||||||
import org.oxycblt.auxio.util.getDimenSize
|
import org.oxycblt.auxio.util.getDimenSize
|
||||||
|
@ -40,7 +41,6 @@ import org.oxycblt.auxio.util.getDrawableCompat
|
||||||
import org.oxycblt.auxio.util.isRtl
|
import org.oxycblt.auxio.util.isRtl
|
||||||
import org.oxycblt.auxio.util.isUnder
|
import org.oxycblt.auxio.util.isUnder
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [RecyclerView] that enables better fast-scrolling. This is fundamentally a implementation of
|
* A [RecyclerView] that enables better fast-scrolling. This is fundamentally a implementation of
|
||||||
|
@ -97,9 +97,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
FastScrollPopupView(context).apply {
|
FastScrollPopupView(context).apply {
|
||||||
layoutParams =
|
layoutParams =
|
||||||
FrameLayout.LayoutParams(
|
FrameLayout.LayoutParams(
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
)
|
|
||||||
.apply {
|
.apply {
|
||||||
gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
|
gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
|
||||||
marginEnd = context.getDimenSize(R.dimen.spacing_small)
|
marginEnd = context.getDimenSize(R.dimen.spacing_small)
|
||||||
|
@ -174,8 +172,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: State) {
|
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: State) {
|
||||||
onPreDraw()
|
onPreDraw()
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
// We use a listener instead of overriding onTouchEvent so that we don't conflict with
|
// We use a listener instead of overriding onTouchEvent so that we don't conflict with
|
||||||
// RecyclerView touch events.
|
// RecyclerView touch events.
|
||||||
|
@ -191,8 +188,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return onItemTouch(event)
|
return onItemTouch(event)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- RECYCLERVIEW EVENT MANAGEMENT ---
|
// --- RECYCLERVIEW EVENT MANAGEMENT ---
|
||||||
|
@ -247,8 +243,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
thumbWidth +
|
thumbWidth +
|
||||||
popupLayoutParams.leftMargin +
|
popupLayoutParams.leftMargin +
|
||||||
popupLayoutParams.rightMargin,
|
popupLayoutParams.rightMargin,
|
||||||
popupLayoutParams.width
|
popupLayoutParams.width)
|
||||||
)
|
|
||||||
|
|
||||||
val heightMeasureSpec =
|
val heightMeasureSpec =
|
||||||
ViewGroup.getChildMeasureSpec(
|
ViewGroup.getChildMeasureSpec(
|
||||||
|
@ -257,8 +252,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
thumbPadding.bottom +
|
thumbPadding.bottom +
|
||||||
popupLayoutParams.topMargin +
|
popupLayoutParams.topMargin +
|
||||||
popupLayoutParams.bottomMargin,
|
popupLayoutParams.bottomMargin,
|
||||||
popupLayoutParams.height
|
popupLayoutParams.height)
|
||||||
)
|
|
||||||
|
|
||||||
popupView.measure(widthMeasureSpec, heightMeasureSpec)
|
popupView.measure(widthMeasureSpec, heightMeasureSpec)
|
||||||
}
|
}
|
||||||
|
@ -279,8 +273,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
(thumbTop + thumbAnchorY - popupAnchorY)
|
(thumbTop + thumbAnchorY - popupAnchorY)
|
||||||
.coerceAtLeast(thumbPadding.top + popupLayoutParams.topMargin)
|
.coerceAtLeast(thumbPadding.top + popupLayoutParams.topMargin)
|
||||||
.coerceAtMost(
|
.coerceAtMost(
|
||||||
height - thumbPadding.bottom - popupLayoutParams.bottomMargin - popupHeight
|
height - thumbPadding.bottom - popupLayoutParams.bottomMargin - popupHeight)
|
||||||
)
|
|
||||||
|
|
||||||
popupView.layout(popupLeft, popupTop, popupLeft + popupWidth, popupTop + popupHeight)
|
popupView.layout(popupLeft, popupTop, popupLeft + popupWidth, popupTop + popupHeight)
|
||||||
}
|
}
|
||||||
|
@ -355,8 +348,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
MotionEvent.ACTION_MOVE -> {
|
MotionEvent.ACTION_MOVE -> {
|
||||||
if (!dragging &&
|
if (!dragging &&
|
||||||
thumbView.isUnder(downX, thumbView.top.toFloat(), minTouchTargetSize) &&
|
thumbView.isUnder(downX, thumbView.top.toFloat(), minTouchTargetSize) &&
|
||||||
abs(eventY - downY) > touchSlop
|
abs(eventY - downY) > touchSlop) {
|
||||||
) {
|
|
||||||
if (thumbView.isUnder(downX, downY, minTouchTargetSize)) {
|
if (thumbView.isUnder(downX, downY, minTouchTargetSize)) {
|
||||||
dragStartY = lastY
|
dragStartY = lastY
|
||||||
dragStartThumbOffset = thumbOffset
|
dragStartThumbOffset = thumbOffset
|
||||||
|
|
|
@ -49,8 +49,8 @@ abstract class MusicFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
||||||
protected val navModel: NavigationViewModel by activityViewModels()
|
protected val navModel: NavigationViewModel by activityViewModels()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the UI flow to perform a specific [PickerMode] action with a particular
|
* Run the UI flow to perform a specific [PickerMode] action with a particular artist from
|
||||||
* artist from [song].
|
* [song].
|
||||||
*/
|
*/
|
||||||
fun doArtistDependentAction(song: Song, mode: PickerMode) {
|
fun doArtistDependentAction(song: Song, mode: PickerMode) {
|
||||||
if (song.artists.size == 1) {
|
if (song.artists.size == 1) {
|
||||||
|
@ -61,24 +61,18 @@ abstract class MusicFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
||||||
} else {
|
} else {
|
||||||
navModel.mainNavigateTo(
|
navModel.mainNavigateTo(
|
||||||
MainNavigationAction.Directions(
|
MainNavigationAction.Directions(
|
||||||
MainFragmentDirections.actionPickArtist(song.uid, mode)
|
MainFragmentDirections.actionPickArtist(song.uid, mode)))
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Run the UI flow to navigate to a particular artist from [album]. */
|
||||||
* Run the UI flow to navigate to a particular artist from [album].
|
|
||||||
*/
|
|
||||||
fun navigateToArtist(album: Album) {
|
fun navigateToArtist(album: Album) {
|
||||||
if (album.artists.size == 1) {
|
if (album.artists.size == 1) {
|
||||||
navModel.exploreNavigateTo(album.artists[0])
|
navModel.exploreNavigateTo(album.artists[0])
|
||||||
} else {
|
} else {
|
||||||
navModel.mainNavigateTo(
|
navModel.mainNavigateTo(
|
||||||
MainNavigationAction.Directions(
|
MainNavigationAction.Directions(
|
||||||
MainFragmentDirections.actionPickArtist(album.uid, PickerMode.SHOW)
|
MainFragmentDirections.actionPickArtist(album.uid, PickerMode.SHOW)))
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,9 +102,7 @@ abstract class MusicFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
||||||
R.id.action_song_detail -> {
|
R.id.action_song_detail -> {
|
||||||
navModel.mainNavigateTo(
|
navModel.mainNavigateTo(
|
||||||
MainNavigationAction.Directions(
|
MainNavigationAction.Directions(
|
||||||
MainFragmentDirections.actionShowDetails(song.uid)
|
MainFragmentDirections.actionShowDetails(song.uid)))
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
error("Unexpected menu item selected")
|
error("Unexpected menu item selected")
|
||||||
|
|
|
@ -26,10 +26,10 @@ import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
|
||||||
import kotlin.properties.ReadOnlyProperty
|
import kotlin.properties.ReadOnlyProperty
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A dialog fragment enabling ViewBinding inflation and usage across the dialog fragment lifecycle.
|
* A dialog fragment enabling ViewBinding inflation and usage across the dialog fragment lifecycle.
|
||||||
|
|
|
@ -23,10 +23,10 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
|
||||||
import kotlin.properties.ReadOnlyProperty
|
import kotlin.properties.ReadOnlyProperty
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A fragment enabling ViewBinding inflation and usage across the fragment lifecycle.
|
* A fragment enabling ViewBinding inflation and usage across the fragment lifecycle.
|
||||||
|
|
|
@ -106,8 +106,6 @@ abstract class DialogViewHolder(root: View) : RecyclerView.ViewHolder(root) {
|
||||||
// Actually make the item full-width, which it won't be in dialogs
|
// Actually make the item full-width, which it won't be in dialogs
|
||||||
root.layoutParams =
|
root.layoutParams =
|
||||||
RecyclerView.LayoutParams(
|
RecyclerView.LayoutParams(
|
||||||
RecyclerView.LayoutParams.MATCH_PARENT,
|
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||||
RecyclerView.LayoutParams.WRAP_CONTENT
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,8 +49,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
initialPadding.left,
|
initialPadding.left,
|
||||||
initialPadding.top,
|
initialPadding.top,
|
||||||
initialPadding.right,
|
initialPadding.right,
|
||||||
initialPadding.bottom + insets.systemBarInsetsCompat.bottom
|
initialPadding.bottom + insets.systemBarInsetsCompat.bottom)
|
||||||
)
|
|
||||||
|
|
||||||
return insets
|
return insets
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,8 +126,7 @@ class SyncListDiffer<T>(
|
||||||
throw AssertionError()
|
throw AssertionError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
field = newList
|
field = newList
|
||||||
result.dispatchUpdatesTo(updateCallback)
|
result.dispatchUpdatesTo(updateCallback)
|
||||||
|
|
|
@ -68,15 +68,13 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
ViewGroup.getChildMeasureSpec(
|
ViewGroup.getChildMeasureSpec(
|
||||||
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
||||||
0,
|
0,
|
||||||
divider.layoutParams.width
|
divider.layoutParams.width)
|
||||||
)
|
|
||||||
|
|
||||||
val heightMeasureSpec =
|
val heightMeasureSpec =
|
||||||
ViewGroup.getChildMeasureSpec(
|
ViewGroup.getChildMeasureSpec(
|
||||||
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
|
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
|
||||||
0,
|
0,
|
||||||
divider.layoutParams.height
|
divider.layoutParams.height)
|
||||||
)
|
|
||||||
|
|
||||||
divider.measure(widthMeasureSpec, heightMeasureSpec)
|
divider.measure(widthMeasureSpec, heightMeasureSpec)
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,8 +63,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||||
val DIFFER =
|
val DIFFER =
|
||||||
object : SimpleItemCallback<Song>() {
|
object : SimpleItemCallback<Song>() {
|
||||||
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
|
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
|
||||||
oldItem.rawName == newItem.rawName &&
|
oldItem.rawName == newItem.rawName && oldItem.areArtistContentsTheSame(newItem)
|
||||||
oldItem.areArtistContentsTheSame(newItem)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,16 +118,16 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
|
||||||
binding.parentImage.bind(item)
|
binding.parentImage.bind(item)
|
||||||
binding.parentName.text = item.resolveName(binding.context)
|
binding.parentName.text = item.resolveName(binding.context)
|
||||||
|
|
||||||
binding.parentInfo.text = if (item.songs.isNotEmpty()) {
|
binding.parentInfo.text =
|
||||||
binding.context.getString(
|
if (item.songs.isNotEmpty()) {
|
||||||
R.string.fmt_two,
|
binding.context.getString(
|
||||||
binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size),
|
R.string.fmt_two,
|
||||||
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size)
|
binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size),
|
||||||
)
|
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
|
||||||
} else {
|
} else {
|
||||||
// Artist has no songs, only display an album count.
|
// Artist has no songs, only display an album count.
|
||||||
binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size)
|
binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||||
binding.root.setOnLongClickListener {
|
binding.root.setOnLongClickListener {
|
||||||
|
@ -172,8 +171,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
|
||||||
binding.context.getString(
|
binding.context.getString(
|
||||||
R.string.fmt_two,
|
R.string.fmt_two,
|
||||||
binding.context.getPlural(R.plurals.fmt_artist_count, item.artists.size),
|
binding.context.getPlural(R.plurals.fmt_artist_count, item.artists.size),
|
||||||
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size)
|
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
|
||||||
)
|
|
||||||
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||||
binding.root.setOnLongClickListener {
|
binding.root.setOnLongClickListener {
|
||||||
listener.onOpenMenu(item, it)
|
listener.onOpenMenu(item, it)
|
||||||
|
|
|
@ -37,9 +37,9 @@ import androidx.annotation.PluralsRes
|
||||||
import androidx.annotation.Px
|
import androidx.annotation.Px
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import kotlin.reflect.KClass
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.MainActivity
|
import org.oxycblt.auxio.MainActivity
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
/** Shortcut to get a [LayoutInflater] from a [Context] */
|
/** Shortcut to get a [LayoutInflater] from a [Context] */
|
||||||
val Context.inflater: LayoutInflater
|
val Context.inflater: LayoutInflater
|
||||||
|
@ -152,8 +152,7 @@ fun Context.newMainPendingIntent(): PendingIntent =
|
||||||
this,
|
this,
|
||||||
IntegerTable.REQUEST_CODE,
|
IntegerTable.REQUEST_CODE,
|
||||||
Intent(this, MainActivity::class.java),
|
Intent(this, MainActivity::class.java),
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0)
|
||||||
)
|
|
||||||
|
|
||||||
/** Create a broadcast [PendingIntent] */
|
/** Create a broadcast [PendingIntent] */
|
||||||
fun Context.newBroadcastPendingIntent(what: String): PendingIntent =
|
fun Context.newBroadcastPendingIntent(what: String): PendingIntent =
|
||||||
|
@ -161,5 +160,4 @@ fun Context.newBroadcastPendingIntent(what: String): PendingIntent =
|
||||||
this,
|
this,
|
||||||
IntegerTable.REQUEST_CODE,
|
IntegerTable.REQUEST_CODE,
|
||||||
Intent(what).setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY),
|
Intent(what).setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY),
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0)
|
||||||
)
|
|
||||||
|
|
|
@ -231,8 +231,7 @@ val WindowInsets.systemGestureInsetsCompat: Insets
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||||
Insets.max(
|
Insets.max(
|
||||||
getCompatInsets(WindowInsets.Type.systemGestures()),
|
getCompatInsets(WindowInsets.Type.systemGestures()),
|
||||||
getCompatInsets(WindowInsets.Type.systemBars())
|
getCompatInsets(WindowInsets.Type.systemBars()))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
|
@ -247,8 +246,7 @@ fun WindowInsets.getSystemWindowCompatInsets() =
|
||||||
systemWindowInsetLeft,
|
systemWindowInsetLeft,
|
||||||
systemWindowInsetTop,
|
systemWindowInsetTop,
|
||||||
systemWindowInsetRight,
|
systemWindowInsetRight,
|
||||||
systemWindowInsetBottom
|
systemWindowInsetBottom)
|
||||||
)
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
@ -272,8 +270,7 @@ fun WindowInsets.replaceSystemBarInsetsCompat(
|
||||||
WindowInsets.Builder(this)
|
WindowInsets.Builder(this)
|
||||||
.setInsets(
|
.setInsets(
|
||||||
WindowInsets.Type.systemBars(),
|
WindowInsets.Type.systemBars(),
|
||||||
Insets.of(left, top, right, bottom).toPlatformInsets()
|
Insets.of(left, top, right, bottom).toPlatformInsets())
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
|
|
@ -50,14 +50,14 @@ private val Any.autoTag: String
|
||||||
get() = "Auxio.${this::class.simpleName ?: "Anonymous Object"}"
|
get() = "Auxio.${this::class.simpleName ?: "Anonymous Object"}"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: If you are politely forking this project while keeping the source open, you can ignore
|
* Note: If you are politely forking this project while keeping the source open, you can ignore the
|
||||||
* the following passage. If not, give me a moment of your time.
|
* following passage. If not, give me a moment of your time.
|
||||||
*
|
*
|
||||||
* Consider what you are doing with your life, plagiarizers. Do you want to live a fulfilling
|
* Consider what you are doing with your life, plagiarizers. Do you want to live a fulfilling
|
||||||
* existence on this planet? Or do you want to spend your life taking work others did and making
|
* existence on this planet? Or do you want to spend your life taking work others did and making it
|
||||||
* it objectively worse so you could arbitrage a fraction of a penny on every AdMob impression you
|
* objectively worse so you could arbitrage a fraction of a penny on every AdMob impression you get?
|
||||||
* get? You could do so many great things if you simply had the courage to come up with an idea of
|
* You could do so many great things if you simply had the courage to come up with an idea of your
|
||||||
* your own.
|
* own.
|
||||||
*
|
*
|
||||||
* If you still want to go on, I guess the only thing I can say is this:
|
* If you still want to go on, I guess the only thing I can say is this:
|
||||||
*
|
*
|
||||||
|
@ -82,13 +82,11 @@ private val Any.autoTag: String
|
||||||
@Suppress("KotlinConstantConditions")
|
@Suppress("KotlinConstantConditions")
|
||||||
private fun basedCopyleftNotice(): Boolean {
|
private fun basedCopyleftNotice(): Boolean {
|
||||||
if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" &&
|
if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" &&
|
||||||
BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug"
|
BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug") {
|
||||||
) {
|
|
||||||
Log.d(
|
Log.d(
|
||||||
"Auxio Project",
|
"Auxio Project",
|
||||||
"Friendly reminder: Auxio is licensed under the " +
|
"Friendly reminder: Auxio is licensed under the " +
|
||||||
"GPLv3 and all derivative apps must be made open source!"
|
"GPLv3 and all derivative apps must be made open source!")
|
||||||
)
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,10 +18,10 @@
|
||||||
package org.oxycblt.auxio.util
|
package org.oxycblt.auxio.util
|
||||||
|
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import org.oxycblt.auxio.BuildConfig
|
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
import org.oxycblt.auxio.BuildConfig
|
||||||
|
|
||||||
/** Assert that we are on a background thread. */
|
/** Assert that we are on a background thread. */
|
||||||
fun requireBackgroundThread() {
|
fun requireBackgroundThread() {
|
||||||
|
|
|
@ -126,8 +126,7 @@ private fun RemoteViews.applyCover(
|
||||||
setImageViewBitmap(R.id.widget_cover, state.cover)
|
setImageViewBitmap(R.id.widget_cover, state.cover)
|
||||||
setContentDescription(
|
setContentDescription(
|
||||||
R.id.widget_cover,
|
R.id.widget_cover,
|
||||||
context.getString(R.string.desc_album_cover, state.song.album.resolveName(context))
|
context.getString(R.string.desc_album_cover, state.song.album.resolveName(context)))
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
setImageViewResource(R.id.widget_cover, R.drawable.ic_remote_default_cover_24)
|
setImageViewResource(R.id.widget_cover, R.drawable.ic_remote_default_cover_24)
|
||||||
setContentDescription(R.id.widget_cover, context.getString(R.string.desc_no_cover))
|
setContentDescription(R.id.widget_cover, context.getString(R.string.desc_no_cover))
|
||||||
|
@ -145,8 +144,7 @@ private fun RemoteViews.applyPlayPauseControls(
|
||||||
|
|
||||||
setOnClickPendingIntent(
|
setOnClickPendingIntent(
|
||||||
R.id.widget_play_pause,
|
R.id.widget_play_pause,
|
||||||
context.newBroadcastPendingIntent(PlaybackService.ACTION_PLAY_PAUSE)
|
context.newBroadcastPendingIntent(PlaybackService.ACTION_PLAY_PAUSE))
|
||||||
)
|
|
||||||
|
|
||||||
// Like the Android 13 media controls, use a circular fab when paused, and a squircle fab
|
// Like the Android 13 media controls, use a circular fab when paused, and a squircle fab
|
||||||
// when playing.
|
// when playing.
|
||||||
|
@ -175,14 +173,10 @@ private fun RemoteViews.applyBasicControls(
|
||||||
applyPlayPauseControls(context, state)
|
applyPlayPauseControls(context, state)
|
||||||
|
|
||||||
setOnClickPendingIntent(
|
setOnClickPendingIntent(
|
||||||
R.id.widget_skip_prev,
|
R.id.widget_skip_prev, context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_PREV))
|
||||||
context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_PREV)
|
|
||||||
)
|
|
||||||
|
|
||||||
setOnClickPendingIntent(
|
setOnClickPendingIntent(
|
||||||
R.id.widget_skip_next,
|
R.id.widget_skip_next, context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_NEXT))
|
||||||
context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_NEXT)
|
|
||||||
)
|
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -195,13 +189,11 @@ private fun RemoteViews.applyFullControls(
|
||||||
|
|
||||||
setOnClickPendingIntent(
|
setOnClickPendingIntent(
|
||||||
R.id.widget_repeat,
|
R.id.widget_repeat,
|
||||||
context.newBroadcastPendingIntent(PlaybackService.ACTION_INC_REPEAT_MODE)
|
context.newBroadcastPendingIntent(PlaybackService.ACTION_INC_REPEAT_MODE))
|
||||||
)
|
|
||||||
|
|
||||||
setOnClickPendingIntent(
|
setOnClickPendingIntent(
|
||||||
R.id.widget_shuffle,
|
R.id.widget_shuffle,
|
||||||
context.newBroadcastPendingIntent(PlaybackService.ACTION_INVERT_SHUFFLE)
|
context.newBroadcastPendingIntent(PlaybackService.ACTION_INVERT_SHUFFLE))
|
||||||
)
|
|
||||||
|
|
||||||
val shuffleRes =
|
val shuffleRes =
|
||||||
when {
|
when {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import android.graphics.Bitmap
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import coil.transform.RoundedCornersTransformation
|
import coil.transform.RoundedCornersTransformation
|
||||||
|
import kotlin.math.sqrt
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.image.BitmapProvider
|
import org.oxycblt.auxio.image.BitmapProvider
|
||||||
import org.oxycblt.auxio.image.extractor.SquareFrameTransform
|
import org.oxycblt.auxio.image.extractor.SquareFrameTransform
|
||||||
|
@ -33,7 +34,6 @@ import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.getDimenSize
|
import org.oxycblt.auxio.util.getDimenSize
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import kotlin.math.sqrt
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper around each [WidgetProvider] that plugs into the main Auxio process and updates the
|
* A wrapper around each [WidgetProvider] that plugs into the main Auxio process and updates the
|
||||||
|
@ -109,8 +109,7 @@ class WidgetComponent(private val context: Context) :
|
||||||
.size(computeSize(sw, sh, 10f))
|
.size(computeSize(sw, sh, 10f))
|
||||||
.transformations(
|
.transformations(
|
||||||
SquareFrameTransform.INSTANCE,
|
SquareFrameTransform.INSTANCE,
|
||||||
RoundedCornersTransformation(cornerRadius.toFloat())
|
RoundedCornersTransformation(cornerRadius.toFloat()))
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
// Divide by two to really make sure we aren't hitting the memory limit.
|
// Divide by two to really make sure we aren't hitting the memory limit.
|
||||||
builder.size(computeSize(sw, sh, 2f))
|
builder.size(computeSize(sw, sh, 2f))
|
||||||
|
@ -121,8 +120,7 @@ class WidgetComponent(private val context: Context) :
|
||||||
val state = WidgetState(song, bitmap, isPlaying, repeatMode, isShuffled)
|
val state = WidgetState(song, bitmap, isPlaying, repeatMode, isShuffled)
|
||||||
widget.update(context, state)
|
widget.update(context, state)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun computeSize(sw: Int, sh: Int, modifier: Float) =
|
private fun computeSize(sw: Int, sh: Int, modifier: Float) =
|
||||||
|
@ -147,8 +145,7 @@ class WidgetComponent(private val context: Context) :
|
||||||
override fun onRepeatChanged(repeatMode: RepeatMode) = update()
|
override fun onRepeatChanged(repeatMode: RepeatMode) = update()
|
||||||
override fun onSettingChanged(key: String) {
|
override fun onSettingChanged(key: String) {
|
||||||
if (key == context.getString(R.string.set_key_cover_mode) ||
|
if (key == context.getString(R.string.set_key_cover_mode) ||
|
||||||
key == context.getString(R.string.set_key_round_mode)
|
key == context.getString(R.string.set_key_round_mode)) {
|
||||||
) {
|
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,8 +60,7 @@ class WidgetProvider : AppWidgetProvider() {
|
||||||
SizeF(180f, 152f) to createSmallWidget(context, state),
|
SizeF(180f, 152f) to createSmallWidget(context, state),
|
||||||
SizeF(272f, 152f) to createWideWidget(context, state),
|
SizeF(272f, 152f) to createWideWidget(context, state),
|
||||||
SizeF(180f, 272f) to createMediumWidget(context, state),
|
SizeF(180f, 272f) to createMediumWidget(context, state),
|
||||||
SizeF(272f, 272f) to createLargeWidget(context, state)
|
SizeF(272f, 272f) to createLargeWidget(context, state))
|
||||||
)
|
|
||||||
|
|
||||||
val awm = AppWidgetManager.getInstance(context)
|
val awm = AppWidgetManager.getInstance(context)
|
||||||
|
|
||||||
|
@ -70,9 +69,7 @@ class WidgetProvider : AppWidgetProvider() {
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logW("Unable to update widget: $e")
|
logW("Unable to update widget: $e")
|
||||||
awm.updateAppWidget(
|
awm.updateAppWidget(
|
||||||
ComponentName(context, this::class.java),
|
ComponentName(context, this::class.java), createDefaultWidget(context))
|
||||||
createDefaultWidget(context)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -170,13 +170,13 @@
|
||||||
<string name="set_lib_tabs">Library tabs</string>
|
<string name="set_lib_tabs">Library tabs</string>
|
||||||
<string name="set_lib_tabs_desc">Change visibility and order of library tabs</string>
|
<string name="set_lib_tabs_desc">Change visibility and order of library tabs</string>
|
||||||
<string name="set_hide_collaborators">Hide collaborators</string>
|
<string name="set_hide_collaborators">Hide collaborators</string>
|
||||||
<string name="set_hide_collaborators_desc">Only show artists that are directly credited on an album in the library</string>
|
<string name="set_hide_collaborators_desc">Only show artists that are directly credited on an album (works best on well-tagged libraries)</string>
|
||||||
<string name="set_cover_mode">Album covers</string>
|
<string name="set_cover_mode">Album covers</string>
|
||||||
<string name="set_cover_mode_off">Off</string>
|
<string name="set_cover_mode_off">Off</string>
|
||||||
<string name="set_cover_mode_media_store">Fast</string>
|
<string name="set_cover_mode_media_store">Fast</string>
|
||||||
<string name="set_cover_mode_quality">High quality</string>
|
<string name="set_cover_mode_quality">High quality</string>
|
||||||
<string name="set_round_mode">Round mode</string>
|
<string name="set_round_mode">Round mode</string>
|
||||||
<string name="set_round_mode_desc">Enable rounded corners on additional UI elements (Requires album covers to be rounded)</string>
|
<string name="set_round_mode_desc">Enable rounded corners on additional UI elements (requires album covers to be rounded)</string>
|
||||||
<string name="set_bar_action">Custom playback bar action</string>
|
<string name="set_bar_action">Custom playback bar action</string>
|
||||||
<!-- Skip to next (song) -->
|
<!-- Skip to next (song) -->
|
||||||
<string name="set_bar_action_next">Skip to next</string>
|
<string name="set_bar_action_next">Skip to next</string>
|
||||||
|
@ -217,13 +217,13 @@
|
||||||
<string name="set_restore_desc">Restore the previously saved playback state (if any)</string>
|
<string name="set_restore_desc">Restore the previously saved playback state (if any)</string>
|
||||||
|
|
||||||
<string name="set_content">Content</string>
|
<string name="set_content">Content</string>
|
||||||
<string name="set_reindex">Reload music</string>
|
<string name="set_reindex">Refresh music</string>
|
||||||
<string name="set_reindex_desc">Reload the music library, using the cache whenever possible</string>
|
<string name="set_reindex_desc">Reload the music library, using cached tags when possible</string>
|
||||||
<!-- Different from "Reload music" -->
|
<!-- Different from "Reload music" -->
|
||||||
<string name="set_rescan">Rescan music</string>
|
<string name="set_rescan">Rescan music</string>
|
||||||
<string name="set_rescan_desc">Reload the music library and re-create the cache (Slower, but more complete)</string>
|
<string name="set_rescan_desc">Clear the tag cache and fully reload the music library (slower, but more complete)</string>
|
||||||
<string name="set_observing">Automatic reloading</string>
|
<string name="set_observing">Automatic reloading</string>
|
||||||
<string name="set_observing_desc">Reload the music library whenever it changes (Requires persistent notification)</string>
|
<string name="set_observing_desc">Reload the music library whenever it changes (requires persistent notification)</string>
|
||||||
<string name="set_dirs">Music folders</string>
|
<string name="set_dirs">Music folders</string>
|
||||||
<string name="set_dirs_desc">Manage where music should be loaded from</string>
|
<string name="set_dirs_desc">Manage where music should be loaded from</string>
|
||||||
<!-- As in the mode to be used with the music folders setting -->
|
<!-- As in the mode to be used with the music folders setting -->
|
||||||
|
|
Loading…
Reference in a new issue