all: lint with ktlint
Just lint with ktlint. It has better import checking functionality.
This commit is contained in:
parent
09823d7829
commit
06d6495dcd
77 changed files with 537 additions and 274 deletions
|
@ -120,10 +120,7 @@ dependencies {
|
||||||
spotless {
|
spotless {
|
||||||
kotlin {
|
kotlin {
|
||||||
target "src/**/*.kt"
|
target "src/**/*.kt"
|
||||||
|
|
||||||
// ktlint does checking, while ktfmt actually does formatting
|
|
||||||
ktlint()
|
ktlint()
|
||||||
ktfmt().dropboxStyle()
|
|
||||||
licenseHeaderFile("NOTICE")
|
licenseHeaderFile("NOTICE")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,8 +47,11 @@ 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() =
|
||||||
|
|
|
@ -98,9 +98,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
val action =
|
val action =
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Intent.ACTION_VIEW -> InternalPlayer.Action.Open(intent.data ?: return false)
|
Intent.ACTION_VIEW -> InternalPlayer.Action.Open(intent.data ?: return false)
|
||||||
AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> {
|
AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll
|
||||||
InternalPlayer.Action.ShuffleAll
|
|
||||||
}
|
|
||||||
else -> return false
|
else -> return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,6 @@ 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
|
||||||
|
@ -51,6 +49,8 @@ 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,9 +89,13 @@ class MainFragment :
|
||||||
|
|
||||||
// Send meaningful accessibility events for bottom sheets
|
// Send meaningful accessibility events for bottom sheets
|
||||||
ViewCompat.setAccessibilityPaneTitle(
|
ViewCompat.setAccessibilityPaneTitle(
|
||||||
binding.playbackSheet, context.getString(R.string.lbl_playback))
|
binding.playbackSheet,
|
||||||
|
context.getString(R.string.lbl_playback)
|
||||||
|
)
|
||||||
ViewCompat.setAccessibilityPaneTitle(
|
ViewCompat.setAccessibilityPaneTitle(
|
||||||
binding.queueSheet, context.getString(R.string.lbl_queue))
|
binding.queueSheet,
|
||||||
|
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) {
|
||||||
|
@ -100,7 +104,8 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -328,14 +333,16 @@ 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
|
||||||
|
|
|
@ -89,7 +89,11 @@ 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.parent, playbackModel.isPlaying, ::updatePlayback)
|
playbackModel.song,
|
||||||
|
playbackModel.parent,
|
||||||
|
playbackModel.isPlaying,
|
||||||
|
::updatePlayback
|
||||||
|
)
|
||||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,7 +168,9 @@ class AlbumDetailFragment :
|
||||||
findNavController()
|
findNavController()
|
||||||
.navigate(
|
.navigate(
|
||||||
AlbumDetailFragmentDirections.actionShowArtist(
|
AlbumDetailFragmentDirections.actionShowArtist(
|
||||||
unlikelyToBeNull(detailModel.currentAlbum.value).artist.uid))
|
unlikelyToBeNull(detailModel.currentAlbum.value).artist.uid
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleItemChange(album: Album?) {
|
private fun handleItemChange(album: Album?) {
|
||||||
|
@ -228,7 +234,8 @@ 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
|
||||||
|
|
|
@ -84,7 +84,11 @@ 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.parent, playbackModel.isPlaying, ::updatePlayback)
|
playbackModel.song,
|
||||||
|
playbackModel.parent,
|
||||||
|
playbackModel.isPlaying,
|
||||||
|
::updatePlayback
|
||||||
|
)
|
||||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -85,7 +85,11 @@ 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.parent, playbackModel.isPlaying, ::updatePlayback)
|
playbackModel.song,
|
||||||
|
playbackModel.parent,
|
||||||
|
playbackModel.isPlaying,
|
||||||
|
::updatePlayback
|
||||||
|
)
|
||||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,14 +74,16 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,7 +127,8 @@ 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.setOnClickListener { listener.onPlayParent() }
|
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
|
||||||
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
|
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
|
||||||
|
|
|
@ -42,7 +42,8 @@ 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") override fun getItemCount() = differ.currentList.size
|
@Suppress("LeakingThis")
|
||||||
|
override fun getItemCount() = differ.currentList.size
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) =
|
override fun getItemViewType(position: Int) =
|
||||||
when (differ.currentList[position]) {
|
when (differ.currentList[position]) {
|
||||||
|
@ -83,7 +84,8 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
|
||||||
return item is Header || item is SortHeader
|
return item is Header || item is SortHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LeakingThis") protected val differ = AsyncListDiffer(this, diffCallback)
|
@Suppress("LeakingThis")
|
||||||
|
protected val differ = AsyncListDiffer(this, diffCallback)
|
||||||
|
|
||||||
override val currentList: List<Item>
|
override val currentList: List<Item>
|
||||||
get() = differ.currentList
|
get() = differ.currentList
|
||||||
|
|
|
@ -36,8 +36,6 @@ 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.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeBinding
|
import org.oxycblt.auxio.databinding.FragmentHomeBinding
|
||||||
|
@ -64,6 +62,8 @@ 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
|
||||||
|
@ -111,7 +111,8 @@ 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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,7 +139,8 @@ 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()
|
||||||
|
@ -197,7 +199,8 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
homeModel.updateCurrentSort(
|
homeModel.updateCurrentSort(
|
||||||
homeModel
|
homeModel
|
||||||
.getSortForDisplay(homeModel.currentTab.value)
|
.getSortForDisplay(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
|
||||||
|
@ -205,7 +208,8 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
homeModel.updateCurrentSort(
|
homeModel.updateCurrentSort(
|
||||||
homeModel
|
homeModel
|
||||||
.getSortForDisplay(homeModel.currentTab.value)
|
.getSortForDisplay(homeModel.currentTab.value)
|
||||||
.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId))))
|
.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId)))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ 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
|
||||||
|
@ -36,6 +35,7 @@ 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.
|
||||||
|
@ -87,7 +87,8 @@ class AlbumListFragment : HomeListFragment<Album>() {
|
||||||
formatter,
|
formatter,
|
||||||
dateAddedMillis,
|
dateAddedMillis,
|
||||||
dateAddedMillis,
|
dateAddedMillis,
|
||||||
DateUtils.FORMAT_ABBREV_ALL)
|
DateUtils.FORMAT_ABBREV_ALL
|
||||||
|
)
|
||||||
.toString()
|
.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ 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.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
@ -38,6 +37,7 @@ 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.
|
||||||
|
@ -59,7 +59,11 @@ class SongListFragment : HomeListFragment<Song>() {
|
||||||
|
|
||||||
collectImmediately(homeModel.songs, homeAdapter::replaceList)
|
collectImmediately(homeModel.songs, homeAdapter::replaceList)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
|
playbackModel.song,
|
||||||
|
playbackModel.parent,
|
||||||
|
playbackModel.isPlaying,
|
||||||
|
::handlePlayback
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPopup(pos: Int): String? {
|
override fun getPopup(pos: Int): String? {
|
||||||
|
@ -93,7 +97,8 @@ class SongListFragment : HomeListFragment<Song>() {
|
||||||
formatter,
|
formatter,
|
||||||
dateAddedMillis,
|
dateAddedMillis,
|
||||||
dateAddedMillis,
|
dateAddedMillis,
|
||||||
DateUtils.FORMAT_ABBREV_ALL)
|
DateUtils.FORMAT_ABBREV_ALL
|
||||||
|
)
|
||||||
.toString()
|
.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,8 @@ sealed class Tab(open val mode: DisplayMode) {
|
||||||
DisplayMode.SHOW_SONGS,
|
DisplayMode.SHOW_SONGS,
|
||||||
DisplayMode.SHOW_ALBUMS,
|
DisplayMode.SHOW_ALBUMS,
|
||||||
DisplayMode.SHOW_ARTISTS,
|
DisplayMode.SHOW_ARTISTS,
|
||||||
DisplayMode.SHOW_GENRES)
|
DisplayMode.SHOW_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 {
|
||||||
|
|
|
@ -91,7 +91,8 @@ 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 =
|
||||||
|
|
|
@ -22,7 +22,6 @@ 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
|
||||||
|
@ -38,8 +37,6 @@ 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
|
||||||
|
@ -48,6 +45,9 @@ 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.
|
||||||
|
@ -194,7 +194,8 @@ 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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,7 +224,9 @@ abstract class BaseFetcher : Fetcher {
|
||||||
// resolution.
|
// resolution.
|
||||||
val bitmap =
|
val bitmap =
|
||||||
SquareFrameTransform.INSTANCE.transform(
|
SquareFrameTransform.INSTANCE.transform(
|
||||||
BitmapFactory.decodeStream(stream), mosaicFrameSize)
|
BitmapFactory.decodeStream(stream),
|
||||||
|
mosaicFrameSize
|
||||||
|
)
|
||||||
|
|
||||||
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
|
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
|
||||||
|
|
||||||
|
@ -241,7 +244,8 @@ 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 {
|
||||||
|
|
|
@ -70,8 +70,10 @@ class BitmapProvider(private val context: Context) {
|
||||||
if (guard.check(handle)) {
|
if (guard.check(handle)) {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,6 @@ 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
|
||||||
|
@ -36,6 +35,7 @@ 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.ui.Sort
|
import org.oxycblt.auxio.ui.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,7 +60,8 @@ 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> {
|
||||||
|
|
|
@ -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
|
||||||
|
@ -101,15 +101,21 @@ 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, indicatorMatrixDst, Matrix.ScaleToFit.CENTER)
|
indicatorMatrixSrc,
|
||||||
|
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, (measuredHeight - iconSize) / 2f)
|
(measuredWidth - iconSize) / 2f,
|
||||||
|
(measuredHeight - iconSize) / 2f
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,7 +94,9 @@ 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, ResourcesCompat.ID_NULL)
|
R.styleable.StyledImageView_staticIcon,
|
||||||
|
ResourcesCompat.ID_NULL
|
||||||
|
)
|
||||||
if (staticIcon != ResourcesCompat.ID_NULL) {
|
if (staticIcon != ResourcesCompat.ID_NULL) {
|
||||||
this.staticIcon = context.getDrawableCompat(staticIcon)
|
this.staticIcon = context.getDrawableCompat(staticIcon)
|
||||||
}
|
}
|
||||||
|
@ -143,7 +145,8 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,11 +21,6 @@ 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.util.UUID
|
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
|
@ -36,6 +31,11 @@ import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.util.inRangeOrNull
|
import org.oxycblt.auxio.util.inRangeOrNull
|
||||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
// --- MUSIC MODELS ---
|
// --- MUSIC MODELS ---
|
||||||
|
|
||||||
|
@ -164,13 +164,15 @@ class Song constructor(raw: Raw) : Music() {
|
||||||
val path =
|
val path =
|
||||||
Path(
|
Path(
|
||||||
name = requireNotNull(raw.displayName) { "Invalid raw: No display name" },
|
name = requireNotNull(raw.displayName) { "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" }
|
||||||
|
@ -238,7 +240,8 @@ class Song constructor(raw: Raw) : Music() {
|
||||||
Artist.Raw(albumArtistName, albumArtistSortName)
|
Artist.Raw(albumArtistName, albumArtistSortName)
|
||||||
} else {
|
} else {
|
||||||
Artist.Raw(artistName, artistSortName)
|
Artist.Raw(artistName, artistSortName)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
val _rawGenres = raw.genreNames?.map { Genre.Raw(it) } ?: listOf(Genre.Raw(null))
|
val _rawGenres = raw.genreNames?.map { Genre.Raw(it) } ?: listOf(Genre.Raw(null))
|
||||||
|
|
||||||
|
@ -505,7 +508,9 @@ 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()
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -535,7 +540,8 @@ 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))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -628,7 +634,8 @@ 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}))?)?)?)?)?$""")
|
"""^(\d{4,})([-.](\d{2})([-.](\d{2})([T ](\d{2})([:.](\d{2})([:.](\d{2}))?)?)?)?)?$"""
|
||||||
|
)
|
||||||
|
|
||||||
fun from(year: Int) = fromTokens(listOf(year))
|
fun from(year: Int) = fromTokens(listOf(year))
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,8 @@ class MusicStore private constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST") fun <T : Music> find(uid: Music.UID): T? = uidMap[uid] as? T
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun <T : Music> find(uid: Music.UID): T? = 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)
|
||||||
|
|
|
@ -24,9 +24,9 @@ import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import java.util.UUID
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
/** Shortcut for making a [ContentResolver] query with less superfluous arguments. */
|
/** Shortcut for making a [ContentResolver] query with less superfluous arguments. */
|
||||||
fun ContentResolver.queryCursor(
|
fun ContentResolver.queryCursor(
|
||||||
|
|
|
@ -26,10 +26,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 java.io.File
|
|
||||||
import java.lang.reflect.Method
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.util.lazyReflectedMethod
|
import org.oxycblt.auxio.util.lazyReflectedMethod
|
||||||
|
import java.io.File
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
/** 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)
|
||||||
|
@ -66,7 +66,9 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
|
||||||
|
|
||||||
fun from(volume: StorageVolume, relativePath: String) =
|
fun from(volume: StorageVolume, relativePath: String) =
|
||||||
Directory(
|
Directory(
|
||||||
volume, relativePath.removePrefix(File.separator).removeSuffix(File.separator))
|
volume,
|
||||||
|
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
|
||||||
|
@ -98,7 +100,8 @@ private val SV_API21_GET_PATH_METHOD: Method by lazyReflectedMethod(StorageVolum
|
||||||
|
|
||||||
/** 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") get() = primaryStorageVolume
|
@Suppress("NewApi")
|
||||||
|
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
|
||||||
|
@ -135,11 +138,13 @@ 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") get() = isPrimary
|
@SuppressLint("NewApi")
|
||||||
|
get() = isPrimary
|
||||||
|
|
||||||
/** If this volume is emulated. */
|
/** If this volume is emulated. */
|
||||||
val StorageVolume.isEmulatedCompat: Boolean
|
val StorageVolume.isEmulatedCompat: Boolean
|
||||||
@SuppressLint("NewApi") get() = isEmulated
|
@SuppressLint("NewApi")
|
||||||
|
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
|
||||||
|
@ -150,11 +155,13 @@ 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") get() = uuid
|
@SuppressLint("NewApi")
|
||||||
|
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") get() = state
|
@SuppressLint("NewApi")
|
||||||
|
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
|
||||||
|
|
|
@ -99,7 +99,8 @@ 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,7 +113,8 @@ 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() }
|
||||||
|
@ -122,7 +124,9 @@ 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, ArrayList(dirAdapter.dirs.map { it.toString() }))
|
KEY_PENDING_DIRS,
|
||||||
|
ArrayList(dirAdapter.dirs.map { it.toString() })
|
||||||
|
)
|
||||||
outState.putBoolean(KEY_PENDING_MODE, isInclude(requireBinding()))
|
outState.putBoolean(KEY_PENDING_MODE, isInclude(requireBinding()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,7 +160,9 @@ 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, DocumentsContract.getTreeDocumentId(uri))
|
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)
|
||||||
|
|
|
@ -31,3 +31,5 @@ class CacheLayer {
|
||||||
|
|
||||||
fun maybePopulateCachedRaw(raw: Song.Raw) = false
|
fun maybePopulateCachedRaw(raw: Song.Raw) = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Make raw naming consistent (always rawSong(s), not raw)
|
||||||
|
|
|
@ -26,7 +26,6 @@ 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.Date
|
import org.oxycblt.auxio.music.Date
|
||||||
import org.oxycblt.auxio.music.Directory
|
import org.oxycblt.auxio.music.Directory
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
@ -38,6 +37,7 @@ 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
|
||||||
|
@ -171,7 +171,9 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||||
projection,
|
projection,
|
||||||
selector,
|
selector,
|
||||||
args.toTypedArray())) { "Content resolver failure: No Cursor returned" }
|
args.toTypedArray()
|
||||||
|
)
|
||||||
|
) { "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)
|
||||||
|
@ -250,7 +252,8 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa
|
||||||
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
|
||||||
|
)
|
||||||
|
|
||||||
abstract val dirSelector: String
|
abstract val dirSelector: String
|
||||||
abstract fun addDirToSelectorArgs(dir: Directory, args: MutableList<String>): Boolean
|
abstract fun addDirToSelectorArgs(dir: Directory, args: MutableList<String>): Boolean
|
||||||
|
@ -287,8 +290,8 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa
|
||||||
raw.artistNames =
|
raw.artistNames =
|
||||||
cursor.getString(artistIndex).run {
|
cursor.getString(artistIndex).run {
|
||||||
if (this != MediaStore.UNKNOWN_STRING) {
|
if (this != MediaStore.UNKNOWN_STRING) {
|
||||||
// While we can't natively parse multi-value tags,
|
// While we can't natively parse multi-value tags from MediaStore itself, we
|
||||||
// from MediaStore itself, we can still parse by user-defined separators.
|
// can still parse by user-defined separators.
|
||||||
maybeParseSeparators(settings)
|
maybeParseSeparators(settings)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
@ -412,7 +415,8 @@ open class BaseApi29MediaStoreLayer(context: Context, cacheLayer: CacheLayer) :
|
||||||
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() =
|
||||||
|
@ -496,7 +500,8 @@ class Api30MediaStoreLayer(context: Context, cacheLayer: CacheLayer) :
|
||||||
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 buildRaw(cursor: Cursor, raw: Song.Raw) {
|
override fun buildRaw(cursor: Cursor, raw: Song.Raw) {
|
||||||
super.buildRaw(cursor, raw)
|
super.buildRaw(cursor, raw)
|
||||||
|
|
|
@ -116,7 +116,8 @@ class Task(context: Context, private val settings: Settings, private val raw: So
|
||||||
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
|
||||||
|
@ -213,9 +214,11 @@ class Task(context: Context, private val settings: Settings, private val raw: So
|
||||||
// 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 }
|
||||||
|
|
||||||
// (Sort) Album
|
// (Sort) Album
|
||||||
|
@ -279,9 +282,11 @@ class Task(context: Context, private val settings: Settings, private val raw: So
|
||||||
// 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 }
|
||||||
|
|
||||||
// (Sort) Album
|
// (Sort) Album
|
||||||
|
|
|
@ -370,4 +370,5 @@ 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"
|
||||||
|
)
|
||||||
|
|
|
@ -152,7 +152,8 @@ class Indexer {
|
||||||
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")
|
||||||
|
|
|
@ -58,7 +58,8 @@ class IndexingNotification(private val context: Context) :
|
||||||
if (indexing.current % 50 == 0) {
|
if (indexing.current % 50 == 0) {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -88,4 +89,6 @@ 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", nameRes = R.string.lbl_indexer)
|
id = BuildConfig.APPLICATION_ID + ".channel.INDEXER",
|
||||||
|
nameRes = R.string.lbl_indexer
|
||||||
|
)
|
||||||
|
|
|
@ -78,7 +78,9 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
wakeLock =
|
wakeLock =
|
||||||
getSystemServiceCompat(PowerManager::class)
|
getSystemServiceCompat(PowerManager::class)
|
||||||
.newWakeLock(
|
.newWakeLock(
|
||||||
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":IndexerService")
|
PowerManager.PARTIAL_WAKE_LOCK,
|
||||||
|
BuildConfig.APPLICATION_ID + ":IndexerService"
|
||||||
|
)
|
||||||
|
|
||||||
settings = Settings(this, this)
|
settings = Settings(this, this)
|
||||||
indexerContentObserver = SystemContentObserver()
|
indexerContentObserver = SystemContentObserver()
|
||||||
|
@ -126,7 +128,8 @@ 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
|
||||||
|
@ -236,7 +239,10 @@ 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, true, this)
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||||
|
true,
|
||||||
|
this
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun release() {
|
fun release() {
|
||||||
|
|
|
@ -122,7 +122,9 @@ 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, playbackModel.currentAudioSessionId)
|
AudioEffect.EXTRA_AUDIO_SESSION,
|
||||||
|
playbackModel.currentAudioSessionId
|
||||||
|
)
|
||||||
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
|
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -60,5 +60,7 @@ class PlaybackSheetBehavior<V : View>(context: Context, attributeSet: AttributeS
|
||||||
MaterialShapeDrawable(sheetBackgroundDrawable.shapeAppearanceModel).apply {
|
MaterialShapeDrawable(sheetBackgroundDrawable.shapeAppearanceModel).apply {
|
||||||
fillColor = sheetBackgroundDrawable.fillColor
|
fillColor = sheetBackgroundDrawable.fillColor
|
||||||
},
|
},
|
||||||
sheetBackgroundDrawable))
|
sheetBackgroundDrawable
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,11 @@ package org.oxycblt.auxio.playback
|
||||||
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.music.formatDurationDs
|
import org.oxycblt.auxio.music.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
|
||||||
|
|
|
@ -131,7 +131,9 @@ 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,7 +42,9 @@ 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.UP or ItemTouchHelper.DOWN) or
|
ItemTouchHelper.ACTION_STATE_DRAG,
|
||||||
|
ItemTouchHelper.UP or ItemTouchHelper.DOWN
|
||||||
|
) or
|
||||||
makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START)
|
makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START)
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
|
@ -132,7 +134,9 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
|
||||||
target: RecyclerView.ViewHolder
|
target: RecyclerView.ViewHolder
|
||||||
) =
|
) =
|
||||||
playbackModel.moveQueueDataItems(
|
playbackModel.moveQueueDataItems(
|
||||||
viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
|
viewHolder.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,13 +61,18 @@ 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.index, playbackModel.isPlaying, ::updateQueue)
|
queueModel.queue,
|
||||||
|
queueModel.index,
|
||||||
|
playbackModel.isPlaying,
|
||||||
|
::updateQueue
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentQueueBinding) {
|
override fun onDestroyBinding(binding: FragmentQueueBinding) {
|
||||||
|
|
|
@ -70,6 +70,10 @@ 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.top, bars.right, expandedOffset + bars.bottom)
|
bars.left,
|
||||||
|
bars.top,
|
||||||
|
bars.right,
|
||||||
|
expandedOffset + bars.bottom
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,8 @@ 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,13 +21,13 @@ 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.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
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.
|
||||||
|
|
|
@ -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,7 +92,8 @@ 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.
|
||||||
|
@ -119,7 +120,8 @@ interface InternalPlayer {
|
||||||
// main playing value is paused.
|
// main playing value is paused.
|
||||||
isPlaying && isAdvancing,
|
isPlaying && isAdvancing,
|
||||||
positionMs,
|
positionMs,
|
||||||
SystemClock.elapsedRealtime())
|
SystemClock.elapsedRealtime()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -116,7 +116,8 @@ 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? {
|
||||||
|
@ -143,7 +144,8 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,7 +181,8 @@ 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,7 +17,6 @@
|
||||||
|
|
||||||
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,6 +30,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager.Callback
|
||||||
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 kotlin.math.max
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master class (and possible god object) for the playback state.
|
* Master class (and possible god object) for the playback state.
|
||||||
|
@ -491,7 +491,8 @@ class PlaybackStateManager private constructor() {
|
||||||
queue = _queue,
|
queue = _queue,
|
||||||
positionMs = playerState.calculateElapsedPosition(),
|
positionMs = playerState.calculateElapsedPosition(),
|
||||||
isShuffled = isShuffled,
|
isShuffled = isShuffled,
|
||||||
repeatMode = repeatMode)
|
repeatMode = repeatMode
|
||||||
|
)
|
||||||
|
|
||||||
// --- CALLBACKS ---
|
// --- CALLBACKS ---
|
||||||
|
|
||||||
|
|
|
@ -140,16 +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.artist.resolveName(context))
|
song.album.artist.resolveName(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(
|
||||||
MediaMetadataCompat.METADATA_KEY_GENRE,
|
MediaMetadataCompat.METADATA_KEY_GENRE,
|
||||||
song.genres.joinToString { it.resolveName(context) })
|
song.genres.joinToString { it.resolveName(context) }
|
||||||
|
)
|
||||||
.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)
|
||||||
|
)
|
||||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs)
|
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs)
|
||||||
|
|
||||||
song.track?.let {
|
song.track?.let {
|
||||||
|
@ -186,7 +189,8 @@ 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>) {
|
||||||
|
@ -223,7 +227,8 @@ 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()
|
||||||
}
|
}
|
||||||
|
@ -234,7 +239,8 @@ 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()
|
||||||
}
|
}
|
||||||
|
@ -320,7 +326,8 @@ 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,19 +375,22 @@ class MediaSessionComponent(private val context: Context, private val callback:
|
||||||
R.drawable.ic_shuffle_on_24
|
R.drawable.ic_shuffle_on_24
|
||||||
} else {
|
} else {
|
||||||
R.drawable.ic_shuffle_off_24
|
R.drawable.ic_shuffle_off_24
|
||||||
})
|
}
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
PlaybackStateCompat.CustomAction.Builder(
|
PlaybackStateCompat.CustomAction.Builder(
|
||||||
PlaybackService.ACTION_INC_REPEAT_MODE,
|
PlaybackService.ACTION_INC_REPEAT_MODE,
|
||||||
context.getString(R.string.desc_change_repeat),
|
context.getString(R.string.desc_change_repeat),
|
||||||
playbackManager.repeatMode.icon)
|
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,10 +52,12 @@ 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))
|
||||||
|
@ -131,7 +133,10 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
|
||||||
): NotificationCompat.Action {
|
): NotificationCompat.Action {
|
||||||
val action =
|
val action =
|
||||||
NotificationCompat.Action.Builder(
|
NotificationCompat.Action.Builder(
|
||||||
iconRes, actionName, context.newBroadcastPendingIntent(actionName))
|
iconRes,
|
||||||
|
actionName,
|
||||||
|
context.newBroadcastPendingIntent(actionName)
|
||||||
|
)
|
||||||
|
|
||||||
return action.build()
|
return action.build()
|
||||||
}
|
}
|
||||||
|
@ -140,6 +145,7 @@ 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,7 +39,6 @@ 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 kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
@ -58,6 +57,7 @@ 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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service that manages the system-side aspects of playback, such as:
|
* A service that manages the system-side aspects of playback, such as:
|
||||||
|
@ -122,8 +122,10 @@ 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
|
||||||
|
@ -138,7 +140,8 @@ 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)
|
||||||
|
@ -278,7 +281,10 @@ class PlaybackService :
|
||||||
override val currentState: InternalPlayer.State
|
override val currentState: InternalPlayer.State
|
||||||
get() =
|
get() =
|
||||||
InternalPlayer.State.new(
|
InternalPlayer.State.new(
|
||||||
player.playWhenReady, player.isPlaying, max(player.currentPosition, 0))
|
player.playWhenReady,
|
||||||
|
player.isPlaying,
|
||||||
|
max(player.currentPosition, 0)
|
||||||
|
)
|
||||||
|
|
||||||
override fun loadSong(song: Song?, play: Boolean) {
|
override fun loadSong(song: Song?, play: Boolean) {
|
||||||
if (song == null) {
|
if (song == null) {
|
||||||
|
@ -315,7 +321,8 @@ 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 */
|
||||||
|
@ -348,7 +355,9 @@ class PlaybackService :
|
||||||
is InternalPlayer.Action.RestoreState -> {
|
is InternalPlayer.Action.RestoreState -> {
|
||||||
restoreScope.launch {
|
restoreScope.launch {
|
||||||
playbackManager.restoreState(
|
playbackManager.restoreState(
|
||||||
PlaybackStateDatabase.getInstance(this@PlaybackService), false)
|
PlaybackStateDatabase.getInstance(this@PlaybackService),
|
||||||
|
false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is InternalPlayer.Action.ShuffleAll -> {
|
is InternalPlayer.Action.ShuffleAll -> {
|
||||||
|
@ -401,7 +410,8 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -463,7 +473,8 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,7 +109,11 @@ class SearchFragment :
|
||||||
|
|
||||||
collectImmediately(searchModel.searchResults, ::handleResults)
|
collectImmediately(searchModel.searchResults, ::handleResults)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
|
playbackModel.song,
|
||||||
|
playbackModel.parent,
|
||||||
|
playbackModel.isPlaying,
|
||||||
|
::handlePlayback
|
||||||
|
)
|
||||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,7 +179,8 @@ 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,7 +23,6 @@ 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.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -42,6 +41,7 @@ import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.util.TaskGuard
|
import org.oxycblt.auxio.util.TaskGuard
|
||||||
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.
|
||||||
|
|
|
@ -84,7 +84,8 @@ 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>) {
|
||||||
|
|
|
@ -77,7 +77,8 @@ 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
|
||||||
|
@ -97,7 +98,8 @@ 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 {
|
||||||
|
@ -122,7 +124,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
val barAction: BarAction
|
val barAction: BarAction
|
||||||
get() =
|
get() =
|
||||||
BarAction.fromIntCode(
|
BarAction.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)
|
||||||
|
)
|
||||||
?: BarAction.NEXT
|
?: BarAction.NEXT
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -140,7 +143,8 @@ 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 */
|
||||||
|
@ -148,7 +152,8 @@ 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)
|
||||||
|
@ -162,7 +167,10 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
get() =
|
get() =
|
||||||
PlaybackMode.fromInt(
|
PlaybackMode.fromInt(
|
||||||
inner.getInt(
|
inner.getInt(
|
||||||
context.getString(R.string.set_key_library_song_playback_mode), Int.MIN_VALUE))
|
context.getString(R.string.set_key_library_song_playback_mode),
|
||||||
|
Int.MIN_VALUE
|
||||||
|
)
|
||||||
|
)
|
||||||
?: PlaybackMode.ALL_SONGS
|
?: PlaybackMode.ALL_SONGS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -173,7 +181,10 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
get() =
|
get() =
|
||||||
PlaybackMode.fromInt(
|
PlaybackMode.fromInt(
|
||||||
inner.getInt(
|
inner.getInt(
|
||||||
context.getString(R.string.set_key_detail_song_playback_mode), Int.MIN_VALUE))
|
context.getString(R.string.set_key_detail_song_playback_mode),
|
||||||
|
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
|
||||||
|
@ -201,7 +212,9 @@ 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, inner.getBoolean(context.getString(R.string.set_key_music_dirs_include), false))
|
dirs,
|
||||||
|
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. */
|
||||||
|
@ -209,9 +222,12 @@ 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), musicDirs.shouldInclude)
|
context.getString(R.string.set_key_music_dirs_include),
|
||||||
|
musicDirs.shouldInclude
|
||||||
|
)
|
||||||
apply()
|
apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -233,12 +249,14 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
var searchFilterMode: DisplayMode?
|
var searchFilterMode: DisplayMode?
|
||||||
get() =
|
get() =
|
||||||
DisplayMode.fromInt(
|
DisplayMode.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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -247,7 +265,8 @@ 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 {
|
||||||
|
@ -260,7 +279,8 @@ 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 {
|
||||||
|
@ -273,7 +293,8 @@ 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 {
|
||||||
|
@ -286,7 +307,8 @@ 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 {
|
||||||
|
@ -301,7 +323,10 @@ 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), Int.MIN_VALUE))
|
context.getString(R.string.set_key_detail_album_sort),
|
||||||
|
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
|
||||||
|
@ -322,7 +347,8 @@ 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.ByYear, false)
|
?: Sort(Sort.Mode.ByYear, false)
|
||||||
set(value) {
|
set(value) {
|
||||||
inner.edit {
|
inner.edit {
|
||||||
|
@ -335,7 +361,8 @@ 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,13 +57,18 @@ constructor(
|
||||||
init {
|
init {
|
||||||
val prefAttrs =
|
val prefAttrs =
|
||||||
context.obtainStyledAttributes(
|
context.obtainStyledAttributes(
|
||||||
attrs, R.styleable.IntListPreference, defStyleAttr, defStyleRes)
|
attrs,
|
||||||
|
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) {
|
||||||
|
|
|
@ -49,7 +49,14 @@ 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, this, coordinator, 0, 0, tConsumed, 0)
|
coordinator,
|
||||||
|
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,7 +127,11 @@ class BottomSheetContentBehavior<V : View>(context: Context, attributeSet: Attri
|
||||||
val bars = insets.systemBarInsetsCompat
|
val bars = insets.systemBarInsetsCompat
|
||||||
|
|
||||||
insets.replaceSystemBarInsetsCompat(
|
insets.replaceSystemBarInsetsCompat(
|
||||||
bars.left, bars.top, bars.right, (bars.bottom - consumed).coerceAtLeast(0))
|
bars.left,
|
||||||
|
bars.top,
|
||||||
|
bars.right,
|
||||||
|
(bars.bottom - consumed).coerceAtLeast(0)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
setup = true
|
setup = true
|
||||||
|
@ -141,7 +145,9 @@ 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, View.MeasureSpec.EXACTLY)
|
parent.measuredHeight - consumed,
|
||||||
|
View.MeasureSpec.EXACTLY
|
||||||
|
)
|
||||||
|
|
||||||
child.measure(contentWidthSpec, contentHeightSpec)
|
child.measure(contentWidthSpec, contentHeightSpec)
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,7 +146,8 @@ 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] */
|
||||||
|
@ -164,13 +165,15 @@ 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, BasicComparator.ARTIST) { it.artist },
|
compareByDynamic(ascending, BasicComparator.ARTIST) { it.artist },
|
||||||
compareByDescending(NullableComparator.DATE) { it.date },
|
compareByDescending(NullableComparator.DATE) { it.date },
|
||||||
compareBy(BasicComparator.ALBUM))
|
compareBy(BasicComparator.ALBUM)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sort by the year of an item, only supported by [Album] and [Song] */
|
/** Sort by the year of an item, only supported by [Album] and [Song] */
|
||||||
|
@ -187,12 +190,14 @@ 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,20 +210,27 @@ 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 }, compareBy(BasicComparator.SONG))
|
compareByDynamic(ascending) { it.durationMs },
|
||||||
|
compareBy(BasicComparator.SONG)
|
||||||
|
)
|
||||||
|
|
||||||
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
|
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(ascending) { it.durationMs }, compareBy(BasicComparator.ALBUM))
|
compareByDynamic(ascending) { it.durationMs },
|
||||||
|
compareBy(BasicComparator.ALBUM)
|
||||||
|
)
|
||||||
|
|
||||||
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
|
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(ascending) { it.durationMs },
|
compareByDynamic(ascending) { 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 }, compareBy(BasicComparator.GENRE))
|
compareByDynamic(ascending) { it.durationMs },
|
||||||
|
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. */
|
||||||
|
@ -231,16 +243,21 @@ 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 }, compareBy(BasicComparator.ALBUM))
|
compareByDynamic(ascending) { it.songs.size },
|
||||||
|
compareBy(BasicComparator.ALBUM)
|
||||||
|
)
|
||||||
|
|
||||||
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
|
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(ascending) { it.songs.size },
|
compareByDynamic(ascending) { 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 }, compareBy(BasicComparator.GENRE))
|
compareByDynamic(ascending) { it.songs.size },
|
||||||
|
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]. */
|
||||||
|
@ -255,7 +272,8 @@ 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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -273,7 +291,8 @@ 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] */
|
||||||
|
@ -286,12 +305,15 @@ 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 }, compareBy(BasicComparator.SONG))
|
compareByDynamic(ascending) { it.dateAdded },
|
||||||
|
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(
|
||||||
|
|
|
@ -39,7 +39,8 @@ 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(
|
||||||
|
@ -101,7 +102,8 @@ 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,7 +62,8 @@ 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,7 +165,8 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0)
|
||||||
centerY + radius,
|
centerY + radius,
|
||||||
startAngle,
|
startAngle,
|
||||||
sweepAngle,
|
sweepAngle,
|
||||||
false)
|
false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,6 @@ 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
|
||||||
|
@ -41,6 +40,7 @@ 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,7 +97,9 @@ 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)
|
||||||
|
@ -172,7 +174,8 @@ 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.
|
||||||
|
@ -188,7 +191,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return onItemTouch(event)
|
return onItemTouch(event)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- RECYCLERVIEW EVENT MANAGEMENT ---
|
// --- RECYCLERVIEW EVENT MANAGEMENT ---
|
||||||
|
@ -243,7 +247,8 @@ 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(
|
||||||
|
@ -252,7 +257,8 @@ 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)
|
||||||
}
|
}
|
||||||
|
@ -273,7 +279,8 @@ 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)
|
||||||
}
|
}
|
||||||
|
@ -348,7 +355,8 @@ 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
|
||||||
|
|
|
@ -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 kotlin.properties.ReadOnlyProperty
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
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 kotlin.properties.ReadOnlyProperty
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 kotlin.properties.ReadOnlyProperty
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
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 kotlin.properties.ReadOnlyProperty
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A fragment enabling ViewBinding inflation and usage across the fragment lifecycle.
|
* A fragment enabling ViewBinding inflation and usage across the fragment lifecycle.
|
||||||
|
|
|
@ -106,6 +106,8 @@ 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.WRAP_CONTENT)
|
RecyclerView.LayoutParams.MATCH_PARENT,
|
||||||
|
RecyclerView.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,8 @@ 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,7 +126,8 @@ class SyncListDiffer<T>(
|
||||||
throw AssertionError()
|
throw AssertionError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
field = newList
|
field = newList
|
||||||
result.dispatchUpdatesTo(updateCallback)
|
result.dispatchUpdatesTo(updateCallback)
|
||||||
|
|
|
@ -68,13 +68,15 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,7 +122,8 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
|
||||||
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.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,7 +152,8 @@ 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 =
|
||||||
|
@ -160,4 +161,5 @@ 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,7 +231,8 @@ 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")
|
||||||
|
@ -246,7 +247,8 @@ 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)
|
||||||
|
@ -270,7 +272,8 @@ 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 -> {
|
||||||
|
|
|
@ -82,11 +82,13 @@ 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,11 +18,11 @@
|
||||||
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 java.util.concurrent.CancellationException
|
import java.util.concurrent.CancellationException
|
||||||
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,7 +126,8 @@ 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))
|
||||||
|
@ -144,7 +145,8 @@ 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.
|
||||||
|
@ -173,10 +175,14 @@ private fun RemoteViews.applyBasicControls(
|
||||||
applyPlayPauseControls(context, state)
|
applyPlayPauseControls(context, state)
|
||||||
|
|
||||||
setOnClickPendingIntent(
|
setOnClickPendingIntent(
|
||||||
R.id.widget_skip_prev, context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_PREV))
|
R.id.widget_skip_prev,
|
||||||
|
context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_PREV)
|
||||||
|
)
|
||||||
|
|
||||||
setOnClickPendingIntent(
|
setOnClickPendingIntent(
|
||||||
R.id.widget_skip_next, context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_NEXT))
|
R.id.widget_skip_next,
|
||||||
|
context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_NEXT)
|
||||||
|
)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -189,11 +195,13 @@ 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,7 +22,6 @@ 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.SquareFrameTransform
|
import org.oxycblt.auxio.image.SquareFrameTransform
|
||||||
|
@ -34,6 +33,7 @@ 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,7 +109,8 @@ 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))
|
||||||
|
@ -120,7 +121,8 @@ 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) =
|
||||||
|
@ -146,7 +148,8 @@ class WidgetComponent(private val context: Context) :
|
||||||
override fun onSettingChanged(key: String) {
|
override fun onSettingChanged(key: String) {
|
||||||
if (key == context.getString(R.string.set_key_show_covers) ||
|
if (key == context.getString(R.string.set_key_show_covers) ||
|
||||||
key == context.getString(R.string.set_key_quality_covers) ||
|
key == context.getString(R.string.set_key_quality_covers) ||
|
||||||
key == context.getString(R.string.set_key_round_mode)) {
|
key == context.getString(R.string.set_key_round_mode)
|
||||||
|
) {
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,8 @@ 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,7 +71,9 @@ 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), createDefaultWidget(context))
|
ComponentName(context, this::class.java),
|
||||||
|
createDefaultWidget(context)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -204,14 +204,14 @@
|
||||||
<string name="set_rewind_prev_desc">Rewind before skipping to the previous song</string>
|
<string name="set_rewind_prev_desc">Rewind before skipping to the previous song</string>
|
||||||
<string name="set_repeat_pause">Pause on repeat</string>
|
<string name="set_repeat_pause">Pause on repeat</string>
|
||||||
<string name="set_repeat_pause_desc">Pause when a song repeats</string>
|
<string name="set_repeat_pause_desc">Pause when a song repeats</string>
|
||||||
|
|
||||||
<string name="set_content">Content</string>
|
|
||||||
<string name="set_save_state">Save playback state</string>
|
<string name="set_save_state">Save playback state</string>
|
||||||
<string name="set_save_desc">Save the current playback state now</string>
|
<string name="set_save_desc">Save the current playback state now</string>
|
||||||
<string name="set_wipe_state">Clear playback state</string>
|
<string name="set_wipe_state">Clear playback state</string>
|
||||||
<string name="set_wipe_desc">Clear the previously saved playback state (if any)</string>
|
<string name="set_wipe_desc">Clear the previously saved playback state (if any)</string>
|
||||||
<string name="set_restore_state">Restore playback state</string>
|
<string name="set_restore_state">Restore playback state</string>
|
||||||
<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_reindex">Reload music</string>
|
<string name="set_reindex">Reload music</string>
|
||||||
<string name="set_reindex_desc">May wipe playback state</string>
|
<string name="set_reindex_desc">May wipe playback state</string>
|
||||||
<string name="set_dirs">Music folders</string>
|
<string name="set_dirs">Music folders</string>
|
||||||
|
|
Loading…
Reference in a new issue