all: rework formatting

Do some miscellanious formatting reworks.

1. Remove all instances of m in favor of _. _ is only used when names
collide or if something should be internal.
2. Make fragments apply their own click listeners.
3. Remove instances of inc/dec and replace them with the more
straightfoward + 1 or - 1.
This commit is contained in:
OxygenCobalt 2022-05-11 19:03:56 -06:00
parent 1a9e55e73b
commit d296a3aed9
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
37 changed files with 388 additions and 379 deletions

View file

@ -6,9 +6,6 @@ plugins {
}
android {
compileSdkVersion 32
buildToolsVersion "32.0.0"
defaultConfig {
applicationId "org.oxycblt.auxio"
versionName "2.2.2"
@ -22,6 +19,20 @@ android {
}
}
compileSdkVersion 32
buildToolsVersion "32.0.0"
// ExoPlayer needs Java 8 to compile.
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += "-Xjvm-default=all"
}
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
}
buildTypes {
debug {
debuggable true
@ -35,17 +46,6 @@ android {
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
// ExoPlayer needs Java 8 to compile.
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += "-Xjvm-default=all"
}
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
}
}
afterEvaluate {
@ -102,7 +102,7 @@ dependencies {
implementation "io.coil-kt:coil:2.0.0-rc03"
// Material
implementation "com.google.android.material:material:1.6.0-rc01"
implementation "com.google.android.material:material:1.6.0"
// LeakCanary
debugImplementation "com.squareup.leakcanary:leakcanary-android:2.9.1"

View file

@ -46,8 +46,6 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
*
* TODO: Rework some fragments to use listeners *even more*
*
* TODO: Phase out m for _
*
* TODO: Fix how selection works in the RecyclerViews (doing it poorly right now)
*
* TODO: Rework padding ethos

View file

@ -275,6 +275,6 @@ abstract class BaseFetcher : Fetcher {
private fun Dimension.mosaicSize(): Int {
val size = pxOrElse { 512 }
return if (size.mod(2) != 0) size.inc() else size
return if (size.mod(2) > 0) size + 1 else size
}
}

View file

@ -112,7 +112,7 @@ private constructor(
private val genre: Genre,
) : BaseFetcher() {
override suspend fun fetch(): FetchResult? {
// Don't sort here to preserve compatibility with previous variations of this image.
// Don't sort here to preserve compatibility with previous versions of this image.
val albums = genre.songs.groupBy { it.album }.keys
val results = albums.mapAtMost(4) { album -> fetchArt(context, album) }

View file

@ -22,7 +22,6 @@ import coil.size.Size
import coil.size.pxOrElse
import coil.transform.Transformation
import kotlin.math.min
import org.oxycblt.auxio.util.logE
/**
* A transformation that performs a center crop-style transformation on an image, however unlike the
@ -46,12 +45,7 @@ class SquareFrameTransform : Transformation {
val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
if (dstSize != desiredWidth || dstSize != desiredHeight) {
try {
// Desired size differs from the cropped size, resize the bitmap.
return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
} catch (e: Exception) {
logE(e.stackTraceToString())
}
}
return dst

View file

@ -19,6 +19,7 @@ package org.oxycblt.auxio.detail
import android.content.Context
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.core.view.children
import androidx.navigation.fragment.findNavController
@ -54,23 +55,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
detailModel.setAlbumId(args.albumId)
setupToolbar(unlikelyToBeNull(detailModel.currentAlbum.value), R.menu.menu_album_detail) {
itemId ->
when (itemId) {
R.id.action_play_next -> {
playbackModel.playNext(unlikelyToBeNull(detailModel.currentAlbum.value))
requireContext().showToast(R.string.lbl_queue_added)
true
}
R.id.action_queue_add -> {
playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentAlbum.value))
requireContext().showToast(R.string.lbl_queue_added)
true
}
else -> false
}
}
setupToolbar(unlikelyToBeNull(detailModel.currentAlbum.value), R.menu.menu_album_detail)
requireBinding().detailRecycler.apply {
adapter = detailAdapter
applySpans { pos ->
@ -86,6 +71,22 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
}
override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_play_next -> {
playbackModel.playNext(unlikelyToBeNull(detailModel.currentAlbum.value))
requireContext().showToast(R.string.lbl_queue_added)
true
}
R.id.action_queue_add -> {
playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentAlbum.value))
requireContext().showToast(R.string.lbl_queue_added)
true
}
else -> false
}
}
override fun onItemClick(item: Item) {
if (item is Song) {
playbackModel.playSong(item, PlaybackMode.IN_ALBUM)

View file

@ -18,6 +18,7 @@
package org.oxycblt.auxio.detail
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@ -69,6 +70,8 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
playbackModel.parent.observe(viewLifecycleOwner, ::updateParent)
}
override fun onMenuItemClick(item: MenuItem): Boolean = false
override fun onItemClick(item: Item) {
when (item) {
is Song -> playbackModel.playSong(item, PlaybackMode.IN_ARTIST)

View file

@ -46,11 +46,11 @@ class DetailAppBarLayout
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
EdgeAppBarLayout(context, attrs, defStyleAttr) {
private var mTitleView: AppCompatTextView? = null
private var mRecycler: RecyclerView? = null
private var titleView: AppCompatTextView? = null
private var recycler: RecyclerView? = null
private var titleShown: Boolean? = null
private var mTitleAnimator: ValueAnimator? = null
private var titleAnimator: ValueAnimator? = null
override fun onAttachedToWindow() {
super.onAttachedToWindow()
@ -58,7 +58,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
}
private fun findTitleView(): AppCompatTextView? {
val titleView = mTitleView
val titleView = titleView
if (titleView != null) {
return titleView
}
@ -79,12 +79,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
}
newTitleView.alpha = 0f
mTitleView = newTitleView
this.titleView = newTitleView
return newTitleView
}
private fun findRecyclerView(): RecyclerView {
val recycler = mRecycler
val recycler = recycler
if (recycler != null) {
return recycler
@ -92,7 +92,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
val newRecycler = (parent as ViewGroup).findViewById<RecyclerView>(liftOnScrollTargetViewId)
mRecycler = newRecycler
this.recycler = newRecycler
return newRecycler
}
@ -101,10 +101,10 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
titleShown = visible
val titleAnimator = mTitleAnimator
val titleAnimator = titleAnimator
if (titleAnimator != null) {
titleAnimator.cancel()
mTitleAnimator = null
this.titleAnimator = null
}
val titleView = findTitleView()
@ -121,7 +121,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
if (titleView?.alpha == to) return
mTitleAnimator =
this.titleAnimator =
ValueAnimator.ofFloat(from, to).apply {
addUpdateListener { titleView?.alpha = it.animatedValue as Float }

View file

@ -21,6 +21,7 @@ import android.view.LayoutInflater
import android.view.View
import androidx.annotation.MenuRes
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.Toolbar
import androidx.core.view.children
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@ -39,7 +40,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* A Base [Fragment] implementing the base features shared across all detail fragments.
* @author OxygenCobalt
*/
abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
abstract class DetailFragment :
ViewBindingFragment<FragmentDetailBinding>(), Toolbar.OnMenuItemClickListener {
protected val detailModel: DetailViewModel by activityViewModels()
protected val navModel: NavigationViewModel by activityViewModels()
protected val playbackModel: PlaybackViewModel by activityViewModels()
@ -49,6 +51,7 @@ abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
override fun onDestroyBinding(binding: FragmentDetailBinding) {
super.onDestroyBinding(binding)
binding.detailToolbar.setOnMenuItemClickListener(null)
binding.detailRecycler.adapter = null
}
@ -56,13 +59,8 @@ abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
* Shortcut method for doing setup of the detail toolbar.
* @param data Parent data to use as the toolbar title
* @param menuId Menu resource to use
* @param onMenuClick (Optional) a click listener for that menu
*/
protected fun setupToolbar(
data: MusicParent,
@MenuRes menuId: Int = -1,
onMenuClick: ((itemId: Int) -> Boolean)? = null
) {
protected fun setupToolbar(data: MusicParent, @MenuRes menuId: Int = -1) {
requireBinding().detailToolbar.apply {
title = data.resolveName(context)
@ -71,10 +69,7 @@ abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
}
setNavigationOnClickListener { findNavController().navigateUp() }
onMenuClick?.let { onClick ->
setOnMenuItemClickListener { item -> onClick(item.itemId) }
}
setOnMenuItemClickListener(this@DetailFragment)
}
}

View file

@ -44,13 +44,13 @@ class DetailViewModel : ViewModel() {
private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance()
private val mCurrentAlbum = MutableLiveData<Album?>()
private val _currentAlbum = MutableLiveData<Album?>()
val currentAlbum: LiveData<Album?>
get() = mCurrentAlbum
get() = _currentAlbum
private val mAlbumData = MutableLiveData(listOf<Item>())
private val _albumData = MutableLiveData(listOf<Item>())
val albumData: LiveData<List<Item>>
get() = mAlbumData
get() = _albumData
var albumSort: Sort
get() = settingsManager.detailAlbumSort
@ -59,12 +59,12 @@ class DetailViewModel : ViewModel() {
currentAlbum.value?.let(::refreshAlbumData)
}
private val mCurrentArtist = MutableLiveData<Artist?>()
private val _currentArtist = MutableLiveData<Artist?>()
val currentArtist: LiveData<Artist?>
get() = mCurrentArtist
get() = _currentArtist
private val mArtistData = MutableLiveData(listOf<Item>())
val artistData: LiveData<List<Item>> = mArtistData
private val _artistData = MutableLiveData(listOf<Item>())
val artistData: LiveData<List<Item>> = _artistData
var artistSort: Sort
get() = settingsManager.detailArtistSort
@ -73,12 +73,12 @@ class DetailViewModel : ViewModel() {
currentArtist.value?.let(::refreshArtistData)
}
private val mCurrentGenre = MutableLiveData<Genre?>()
private val _currentGenre = MutableLiveData<Genre?>()
val currentGenre: LiveData<Genre?>
get() = mCurrentGenre
get() = _currentGenre
private val mGenreData = MutableLiveData(listOf<Item>())
val genreData: LiveData<List<Item>> = mGenreData
private val _genreData = MutableLiveData(listOf<Item>())
val genreData: LiveData<List<Item>> = _genreData
var genreSort: Sort
get() = settingsManager.detailGenreSort
@ -88,30 +88,30 @@ class DetailViewModel : ViewModel() {
}
fun setAlbumId(id: Long) {
if (mCurrentAlbum.value?.id == id) return
if (_currentAlbum.value?.id == id) return
val library = unlikelyToBeNull(musicStore.library)
val album =
requireNotNull(library.albums.find { it.id == id }) { "Invalid album id provided " }
mCurrentAlbum.value = album
_currentAlbum.value = album
refreshAlbumData(album)
}
fun setArtistId(id: Long) {
if (mCurrentArtist.value?.id == id) return
if (_currentArtist.value?.id == id) return
val library = unlikelyToBeNull(musicStore.library)
val artist =
requireNotNull(library.artists.find { it.id == id }) { "Invalid artist id provided" }
mCurrentArtist.value = artist
_currentArtist.value = artist
refreshArtistData(artist)
}
fun setGenreId(id: Long) {
if (mCurrentGenre.value?.id == id) return
if (_currentGenre.value?.id == id) return
val library = unlikelyToBeNull(musicStore.library)
val genre =
requireNotNull(library.genres.find { it.id == id }) { "Invalid genre id provided" }
mCurrentGenre.value = genre
_currentGenre.value = genre
refreshGenreData(genre)
}
@ -120,7 +120,7 @@ class DetailViewModel : ViewModel() {
val data = mutableListOf<Item>(genre)
data.add(SortHeader(-2, R.string.lbl_songs))
data.addAll(genreSort.genre(genre))
mGenreData.value = data
_genreData.value = data
}
private fun refreshArtistData(artist: Artist) {
@ -130,7 +130,7 @@ class DetailViewModel : ViewModel() {
data.addAll(Sort.ByYear(false).albums(artist.albums))
data.add(SortHeader(-3, R.string.lbl_songs))
data.addAll(artistSort.artist(artist))
mArtistData.value = data.toList()
_artistData.value = data.toList()
}
private fun refreshAlbumData(album: Album) {
@ -138,6 +138,6 @@ class DetailViewModel : ViewModel() {
val data = mutableListOf<Item>(album)
data.add(SortHeader(id = -2, R.string.lbl_songs))
data.addAll(albumSort.album(album))
mAlbumData.value = data
_albumData.value = data
}
}

View file

@ -18,6 +18,7 @@
package org.oxycblt.auxio.detail
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@ -65,6 +66,8 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
}
override fun onMenuItemClick(item: MenuItem): Boolean = false
override fun onItemClick(item: Item) {
when (item) {
is Song -> playbackModel.playSong(item, PlaybackMode.IN_GENRE)

View file

@ -27,12 +27,12 @@ import org.oxycblt.auxio.databinding.ItemAlbumSongBinding
import org.oxycblt.auxio.databinding.ItemDetailBinding
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.toDuration
import org.oxycblt.auxio.ui.BindingViewHolder
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.SimpleItemCallback
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.getPluralSafe
import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.textSafe
@ -185,7 +185,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
}
binding.songName.textSafe = item.resolveName(binding.context)
binding.songDuration.textSafe = item.seconds.toDuration(false)
binding.songDuration.textSafe = item.seconds.formatDuration(false)
binding.root.apply {
setOnClickListener { listener.onItemClick(item) }

View file

@ -20,6 +20,7 @@ package org.oxycblt.auxio.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import androidx.appcompat.widget.Toolbar
import androidx.core.view.iterator
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@ -60,7 +61,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
*
* TODO: Add duration and song count sorts
*/
class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuItemClickListener {
private val playbackModel: PlaybackViewModel by activityViewModels()
private val navModel: NavigationViewModel by activityViewModels()
private val homeModel: HomeViewModel by activityViewModels()
@ -73,11 +74,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
binding.homeToolbar.apply {
sortItem = menu.findItem(R.id.submenu_sorting)
setOnMenuItemClickListener { item ->
onMenuClick(item)
true
}
setOnMenuItemClickListener(this@HomeFragment)
}
binding.homePager.apply {
@ -103,7 +100,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
// --- VIEWMODEL SETUP ---
homeModel.fastScrolling.observe(viewLifecycleOwner, ::updateFastScrolling)
homeModel.isFastScrolling.observe(viewLifecycleOwner, ::updateFastScrolling)
homeModel.currentTab.observe(viewLifecycleOwner) { tab -> updateCurrentTab(sortItem, tab) }
homeModel.recreateTabs.observe(viewLifecycleOwner, ::handleRecreateTabs)
@ -111,7 +108,12 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
}
private fun onMenuClick(item: MenuItem) {
override fun onDestroyBinding(binding: FragmentHomeBinding) {
super.onDestroyBinding(binding)
binding.homeToolbar.setOnMenuItemClickListener(null)
}
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_search -> {
logD("Navigating to search")
@ -147,6 +149,8 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
.assignId(item.itemId)))
}
}
return true
}
private fun updateFastScrolling(isFastScrolling: Boolean) {

View file

@ -40,21 +40,21 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance()
private val mSongs = MutableLiveData(listOf<Song>())
private val _songs = MutableLiveData(listOf<Song>())
val songs: LiveData<List<Song>>
get() = mSongs
get() = _songs
private val mAlbums = MutableLiveData(listOf<Album>())
private val _albums = MutableLiveData(listOf<Album>())
val albums: LiveData<List<Album>>
get() = mAlbums
get() = _albums
private val mArtists = MutableLiveData(listOf<Artist>())
private val _artists = MutableLiveData(listOf<Artist>())
val artists: LiveData<List<Artist>>
get() = mArtists
get() = _artists
private val mGenres = MutableLiveData(listOf<Genre>())
private val _genres = MutableLiveData(listOf<Genre>())
val genres: LiveData<List<Genre>>
get() = mGenres
get() = _genres
var tabs: List<DisplayMode> = visibleTabs
private set
@ -63,18 +63,18 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
private val visibleTabs: List<DisplayMode>
get() = settingsManager.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
private val mCurrentTab = MutableLiveData(tabs[0])
val currentTab: LiveData<DisplayMode> = mCurrentTab
private val _currentTab = MutableLiveData(tabs[0])
val currentTab: LiveData<DisplayMode> = _currentTab
/**
* Marker to recreate all library tabs, usually initiated by a settings change. When this flag
* is set, all tabs (and their respective viewpager fragments) will be recreated from scratch.
*/
private val mRecreateTabs = MutableLiveData(false)
val recreateTabs: LiveData<Boolean> = mRecreateTabs
private val _shouldRecreateTabs = MutableLiveData(false)
val recreateTabs: LiveData<Boolean> = _shouldRecreateTabs
private val mFastScrolling = MutableLiveData(false)
val fastScrolling: LiveData<Boolean> = mFastScrolling
private val _isFastScrolling = MutableLiveData(false)
val isFastScrolling: LiveData<Boolean> = _isFastScrolling
init {
musicStore.addCallback(this)
@ -84,11 +84,11 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
/** Update the current tab based off of the new ViewPager position. */
fun updateCurrentTab(pos: Int) {
logD("Updating current tab to ${tabs[pos]}")
mCurrentTab.value = tabs[pos]
_currentTab.value = tabs[pos]
}
fun finishRecreateTabs() {
mRecreateTabs.value = false
_shouldRecreateTabs.value = false
}
fun getSortForDisplay(displayMode: DisplayMode): Sort {
@ -102,23 +102,23 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
/** Update the currently displayed item's [Sort]. */
fun updateCurrentSort(sort: Sort) {
logD("Updating ${mCurrentTab.value} sort to $sort")
when (mCurrentTab.value) {
logD("Updating ${_currentTab.value} sort to $sort")
when (_currentTab.value) {
DisplayMode.SHOW_SONGS -> {
settingsManager.libSongSort = sort
mSongs.value = sort.songs(unlikelyToBeNull(mSongs.value))
_songs.value = sort.songs(unlikelyToBeNull(_songs.value))
}
DisplayMode.SHOW_ALBUMS -> {
settingsManager.libAlbumSort = sort
mAlbums.value = sort.albums(unlikelyToBeNull(mAlbums.value))
_albums.value = sort.albums(unlikelyToBeNull(_albums.value))
}
DisplayMode.SHOW_ARTISTS -> {
settingsManager.libArtistSort = sort
mArtists.value = sort.artists(unlikelyToBeNull(mArtists.value))
_artists.value = sort.artists(unlikelyToBeNull(_artists.value))
}
DisplayMode.SHOW_GENRES -> {
settingsManager.libGenreSort = sort
mGenres.value = sort.genres(unlikelyToBeNull(mGenres.value))
_genres.value = sort.genres(unlikelyToBeNull(_genres.value))
}
else -> {}
}
@ -129,7 +129,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
* begins to fast scroll.
*/
fun updateFastScrolling(scrolling: Boolean) {
mFastScrolling.value = scrolling
_isFastScrolling.value = scrolling
}
// --- OVERRIDES ---
@ -137,16 +137,16 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
override fun onMusicUpdate(response: MusicStore.Response) {
if (response is MusicStore.Response.Ok) {
val library = response.library
mSongs.value = settingsManager.libSongSort.songs(library.songs)
mAlbums.value = settingsManager.libAlbumSort.albums(library.albums)
mArtists.value = settingsManager.libArtistSort.artists(library.artists)
mGenres.value = settingsManager.libGenreSort.genres(library.genres)
_songs.value = settingsManager.libSongSort.songs(library.songs)
_albums.value = settingsManager.libAlbumSort.albums(library.albums)
_artists.value = settingsManager.libArtistSort.artists(library.artists)
_genres.value = settingsManager.libGenreSort.genres(library.genres)
}
}
override fun onLibTabsUpdate(libTabs: Array<Tab>) {
tabs = visibleTabs
mRecreateTabs.value = true
_shouldRecreateTabs.value = true
}
override fun onCleared() {

View file

@ -106,7 +106,7 @@ object Indexer {
private val Context.contentResolverSafe: ContentResolver
get() = applicationContext.contentResolver
fun run(context: Context): MusicStore.Library? {
fun index(context: Context): MusicStore.Library? {
val songs = loadSongs(context)
if (songs.isEmpty()) return null
@ -116,14 +116,12 @@ object Indexer {
// Sanity check: Ensure that all songs are linked up to albums/artists/genres.
for (song in songs) {
if (song.internalIsMissingAlbum ||
song.internalIsMissingArtist ||
song.internalIsMissingGenre) {
if (song._isMissingAlbum || song._isMissingArtist || song._isMissingGenre) {
throw IllegalStateException(
"Found malformed song: ${song.rawName} [" +
"album: ${!song.internalIsMissingAlbum} " +
"artist: ${!song.internalIsMissingArtist} " +
"genre: ${!song.internalIsMissingGenre}]")
"album: ${!song._isMissingAlbum} " +
"artist: ${!song._isMissingArtist} " +
"genre: ${!song._isMissingGenre}]")
}
}
@ -242,9 +240,9 @@ object Indexer {
songs
.distinctBy {
it.rawName to
it.internalMediaStoreAlbumName to
it.internalMediaStoreArtistName to
it.internalMediaStoreAlbumArtistName to
it._mediaStoreAlbumName to
it._mediaStoreArtistName to
it._mediaStoreAlbumArtistName to
it.track to
it.duration
}
@ -270,7 +268,7 @@ object Indexer {
*/
private fun buildAlbums(songs: List<Song>): List<Album> {
val albums = mutableListOf<Album>()
val songsByAlbum = songs.groupBy { it.internalAlbumGroupingId }
val songsByAlbum = songs.groupBy { it._albumGroupingId }
for (entry in songsByAlbum) {
val albumSongs = entry.value
@ -289,13 +287,13 @@ object Indexer {
}
}
val albumName = templateSong.internalMediaStoreAlbumName
val albumYear = templateSong.internalMediaStoreYear
val albumName = templateSong._mediaStoreAlbumName
val albumYear = templateSong._mediaStoreYear
val albumCoverUri =
ContentUris.withAppendedId(
Uri.parse("content://media/external/audio/albumart"),
templateSong.internalMediaStoreAlbumId)
val artistName = templateSong.internalGroupingArtistName
templateSong._mediaStoreAlbumId)
val artistName = templateSong._artistGroupingName
albums.add(
Album(
@ -318,14 +316,14 @@ object Indexer {
*/
private fun buildArtists(albums: List<Album>): List<Artist> {
val artists = mutableListOf<Artist>()
val albumsByArtist = albums.groupBy { it.internalArtistGroupingId }
val albumsByArtist = albums.groupBy { it._artistGroupingId }
for (entry in albumsByArtist) {
val templateAlbum = entry.value[0]
val artistName =
when (templateAlbum.internalGroupingArtistName) {
when (templateAlbum._artistGroupingName) {
MediaStore.UNKNOWN_STRING -> null
else -> templateAlbum.internalGroupingArtistName
else -> templateAlbum._artistGroupingName
}
val artistAlbums = entry.value
@ -366,7 +364,7 @@ object Indexer {
}
}
val songsWithoutGenres = songs.filter { it.internalIsMissingGenre }
val songsWithoutGenres = songs.filter { it._isMissingGenre }
if (songsWithoutGenres.isNotEmpty()) {
// Songs that don't have a genre will be thrown into an unknown genre.
val unknownGenre = Genre(null, songsWithoutGenres)
@ -398,9 +396,7 @@ object Indexer {
while (cursor.moveToNext()) {
val id = cursor.getLong(idIndex)
songs.find { it.internalMediaStoreId == id }?.let { song ->
genreSongs.add(song)
}
songs.find { it._mediaStoreId == id }?.let { song -> genreSongs.add(song) }
}
}

View file

@ -24,6 +24,7 @@ import android.provider.MediaStore
import androidx.core.text.isDigitsOnly
import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.unlikelyToBeNull
// --- MUSIC MODELS ---
@ -59,17 +60,17 @@ data class Song(
/** The track number of this song, null if there isn't any. */
val track: Int?,
/** Internal field. Do not use. */
val internalMediaStoreId: Long,
val _mediaStoreId: Long,
/** Internal field. Do not use. */
val internalMediaStoreYear: Int?,
val _mediaStoreYear: Int?,
/** Internal field. Do not use. */
val internalMediaStoreAlbumName: String,
val _mediaStoreAlbumName: String,
/** Internal field. Do not use. */
val internalMediaStoreAlbumId: Long,
val _mediaStoreAlbumId: Long,
/** Internal field. Do not use. */
val internalMediaStoreArtistName: String?,
val _mediaStoreArtistName: String?,
/** Internal field. Do not use. */
val internalMediaStoreAlbumArtistName: String?,
val _mediaStoreAlbumArtistName: String?,
) : Music() {
override val id: Long
get() {
@ -89,68 +90,65 @@ data class Song(
/** The URI for this song. */
val uri: Uri
get() =
ContentUris.withAppendedId(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, internalMediaStoreId)
ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, _mediaStoreId)
/** The duration of this song, in seconds (rounded down) */
val seconds: Long
get() = duration / 1000
private var mAlbum: Album? = null
private var _album: Album? = null
/** The album of this song. */
val album: Album
get() = unlikelyToBeNull(mAlbum)
get() = unlikelyToBeNull(_album)
private var mGenre: Genre? = null
private var _genre: Genre? = null
/** The genre of this song. Will be an "unknown genre" if the song does not have any. */
val genre: Genre
get() = unlikelyToBeNull(mGenre)
get() = unlikelyToBeNull(_genre)
/**
* The raw artist name for this song in particular. First uses the artist tag, and then falls
* back to the album artist tag (i.e parent artist name). Null if name is unknown.
*/
val individualRawArtistName: String?
get() = internalMediaStoreArtistName ?: album.artist.rawName
get() = _mediaStoreArtistName ?: album.artist.rawName
/**
* Resolve the artist name for this song in particular. First uses the artist tag, and then
* falls back to the album artist tag (i.e parent artist name)
*/
fun resolveIndividualArtistName(context: Context) =
internalMediaStoreArtistName ?: album.artist.resolveName(context)
_mediaStoreArtistName ?: album.artist.resolveName(context)
/** Internal field. Do not use. */
val internalAlbumGroupingId: Long
val _albumGroupingId: Long
get() {
var result = internalGroupingArtistName.lowercase().hashCode().toLong()
result = 31 * result + internalMediaStoreAlbumName.lowercase().hashCode()
var result = _artistGroupingName.lowercase().hashCode().toLong()
result = 31 * result + _mediaStoreAlbumName.lowercase().hashCode()
return result
}
/** Internal field. Do not use. */
val internalGroupingArtistName: String
get() =
internalMediaStoreAlbumArtistName
?: internalMediaStoreArtistName ?: MediaStore.UNKNOWN_STRING
val _artistGroupingName: String
get() = _mediaStoreAlbumArtistName ?: _mediaStoreArtistName ?: MediaStore.UNKNOWN_STRING
/** Internal field. Do not use. */
val internalIsMissingAlbum: Boolean
get() = mAlbum == null
val _isMissingAlbum: Boolean
get() = _album == null
/** Internal field. Do not use. */
val internalIsMissingArtist: Boolean
get() = mAlbum?.internalIsMissingArtist ?: true
val _isMissingArtist: Boolean
get() = _album?._isMissingArtist ?: true
/** Internal field. Do not use. */
val internalIsMissingGenre: Boolean
get() = mGenre == null
val _isMissingGenre: Boolean
get() = _genre == null
/** Internal method. Do not use. */
fun internalLinkAlbum(album: Album) {
mAlbum = album
fun _linkAlbum(album: Album) {
_album = album
}
/** Internal method. Do not use. */
fun internalLinkGenre(genre: Genre) {
mGenre = genre
fun _linkGenre(genre: Genre) {
_genre = genre
}
}
@ -164,11 +162,11 @@ data class Album(
/** The songs of this album. */
val songs: List<Song>,
/** Internal field. Do not use. */
val internalGroupingArtistName: String,
val _artistGroupingName: String,
) : MusicParent() {
init {
for (song in songs) {
song.internalLinkAlbum(this)
song._linkAlbum(this)
}
}
@ -187,24 +185,24 @@ data class Album(
/** The formatted total duration of this album */
val totalDuration: String
get() = songs.sumOf { it.seconds }.toDuration(false)
get() = songs.sumOf { it.seconds }.formatDuration(false)
private var mArtist: Artist? = null
private var _artist: Artist? = null
/** The parent artist of this album. */
val artist: Artist
get() = unlikelyToBeNull(mArtist)
get() = unlikelyToBeNull(_artist)
/** Internal field. Do not use. */
val internalArtistGroupingId: Long
get() = internalGroupingArtistName.lowercase().hashCode().toLong()
val _artistGroupingId: Long
get() = _artistGroupingName.lowercase().hashCode().toLong()
/** Internal field. Do not use. */
val internalIsMissingArtist: Boolean
get() = mArtist == null
val _isMissingArtist: Boolean
get() = _artist == null
/** Internal method. Do not use. */
fun internalLinkArtist(artist: Artist) {
mArtist = artist
fun _linkArtist(artist: Artist) {
_artist = artist
}
}
@ -219,7 +217,7 @@ data class Artist(
) : MusicParent() {
init {
for (album in albums) {
album.internalLinkArtist(this)
album._linkArtist(this)
}
}
@ -239,7 +237,7 @@ data class Artist(
data class Genre(override val rawName: String?, val songs: List<Song>) : MusicParent() {
init {
for (song in songs) {
song.internalLinkGenre(this)
song._linkGenre(this)
}
}
@ -254,7 +252,7 @@ data class Genre(override val rawName: String?, val songs: List<Song>) : MusicPa
/** The formatted total duration of this genre */
val totalDuration: String
get() = songs.sumOf { it.seconds }.toDuration(false)
get() = songs.sumOf { it.seconds }.formatDuration(false)
}
/**

View file

@ -60,16 +60,16 @@ class MusicStore private constructor() {
}
/** Load/Sort the entire music library. Should always be ran on a coroutine. */
suspend fun index(context: Context): Response {
suspend fun load(context: Context): Response {
logD("Starting initial music load")
val newResponse = withContext(Dispatchers.IO) { indexImpl(context) }.also { response = it }
val newResponse = withContext(Dispatchers.IO) { loadImpl(context) }.also { response = it }
for (callback in callbacks) {
callback.onMusicUpdate(newResponse)
}
return newResponse
}
private fun indexImpl(context: Context): Response {
private fun loadImpl(context: Context): Response {
val notGranted =
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_DENIED
@ -81,7 +81,7 @@ class MusicStore private constructor() {
val response =
try {
val start = System.currentTimeMillis()
val library = Indexer.run(context)
val library = Indexer.index(context)
if (library != null) {
logD(
"Music load completed successfully in ${System.currentTimeMillis() - start}ms")
@ -132,8 +132,6 @@ class MusicStore private constructor() {
/**
* A response that [MusicStore] returns when loading music. And before you ask, yes, I do like
* rust.
*
* TODO: Add the exception to the "FAILED" ErrorKind
*/
sealed class Response {
class Ok(val library: Library) : Response()

View file

@ -17,28 +17,9 @@
package org.oxycblt.auxio.music
import android.text.format.DateUtils
import org.oxycblt.auxio.util.logD
// --- EXTENSION FUNCTIONS ---
/**
* Convert a [Long] of seconds into a string duration.
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
* will be returned if the second value is 0.
*/
fun Long.toDuration(isElapsed: Boolean): String {
if (!isElapsed && this == 0L) {
logD("Non-elapsed duration is zero, using --:--")
return "--:--"
}
var durationString = DateUtils.formatElapsedTime(this)
// If the duration begins with a excess zero [e.g 01:42], then cut it off.
if (durationString[0] == '0') {
durationString = durationString.slice(1 until durationString.length)
}
return durationString
}

View file

@ -28,8 +28,8 @@ import org.oxycblt.auxio.util.logD
class MusicViewModel : ViewModel(), MusicStore.Callback {
private val musicStore = MusicStore.getInstance()
private val mLoaderResponse = MutableLiveData<MusicStore.Response?>(null)
val loaderResponse: LiveData<MusicStore.Response?> = mLoaderResponse
private val _loaderResponse = MutableLiveData<MusicStore.Response?>(null)
val loaderResponse: LiveData<MusicStore.Response?> = _loaderResponse
private var isBusy = false
@ -42,29 +42,29 @@ class MusicViewModel : ViewModel(), MusicStore.Callback {
* navigated to and because SnackBars will have the best UX here.
*/
fun loadMusic(context: Context) {
if (mLoaderResponse.value != null || isBusy) {
if (_loaderResponse.value != null || isBusy) {
logD("Loader is busy/already completed, not reloading")
return
}
isBusy = true
mLoaderResponse.value = null
_loaderResponse.value = null
viewModelScope.launch {
val result = musicStore.index(context)
mLoaderResponse.value = result
val result = musicStore.load(context)
_loaderResponse.value = result
isBusy = false
}
}
fun reloadMusic(context: Context) {
logD("Reloading music library")
mLoaderResponse.value = null
_loaderResponse.value = null
loadMusic(context)
}
override fun onMusicUpdate(response: MusicStore.Response) {
mLoaderResponse.value = response
_loaderResponse.value = response
}
override fun onCleared() {

View file

@ -29,7 +29,7 @@ import org.oxycblt.auxio.util.requireBackgroundThread
/**
* Database for storing excluded directories. Note that the paths stored here will not work with
* MediaStore unless you append a "%" at the end. Yes. I know Room exists. But that would needlessly
* bloat my app and has crippling bugs.
* bloat my app and has crippling bugs. TODO: Migrate this to SharedPreferences?
* @author OxygenCobalt
*/
class ExcludedDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {

View file

@ -37,9 +37,9 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* TODO: Unify with MusicViewModel
*/
class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewModel() {
private val mPaths = MutableLiveData(mutableListOf<String>())
private val _paths = MutableLiveData(mutableListOf<String>())
val paths: LiveData<MutableList<String>>
get() = mPaths
get() = _paths
var isModified: Boolean = false
private set
@ -53,10 +53,10 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
* called.
*/
fun addPath(path: String) {
val paths = unlikelyToBeNull(mPaths.value)
val paths = unlikelyToBeNull(_paths.value)
if (!paths.contains(path)) {
paths.add(path)
mPaths.value = mPaths.value
_paths.value = _paths.value
isModified = true
}
}
@ -66,8 +66,8 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
* [save] is called.
*/
fun removePath(path: String) {
unlikelyToBeNull(mPaths.value).remove(path)
mPaths.value = mPaths.value
unlikelyToBeNull(_paths.value).remove(path)
_paths.value = _paths.value
isModified = true
}
@ -75,7 +75,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
fun save(onDone: () -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val start = System.currentTimeMillis()
excludedDatabase.writePaths(unlikelyToBeNull(mPaths.value))
excludedDatabase.writePaths(unlikelyToBeNull(_paths.value))
isModified = false
onDone()
this@ExcludedViewModel.logD(
@ -90,7 +90,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
isModified = false
val dbPaths = excludedDatabase.readPaths()
withContext(Dispatchers.Main) { mPaths.value = dbPaths.toMutableList() }
withContext(Dispatchers.Main) { _paths.value = dbPaths.toMutableList() }
this@ExcludedViewModel.logD(
"Path load completed successfully in ${System.currentTimeMillis() - start}ms")

View file

@ -20,6 +20,7 @@ package org.oxycblt.auxio.playback
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import androidx.appcompat.widget.Toolbar
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@ -31,12 +32,12 @@ import org.oxycblt.auxio.coil.bindAlbumCover
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.toDuration
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.clamp
import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.getAttrColorSafe
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.stateList
@ -55,7 +56,8 @@ import org.oxycblt.auxio.util.textSafe
class PlaybackPanelFragment :
ViewBindingFragment<FragmentPlaybackPanelBinding>(),
Slider.OnChangeListener,
Slider.OnSliderTouchListener {
Slider.OnSliderTouchListener,
Toolbar.OnMenuItemClickListener {
private val playbackModel: PlaybackViewModel by activityViewModels()
private val navModel: NavigationViewModel by activityViewModels()
@ -77,16 +79,7 @@ class PlaybackPanelFragment :
binding.playbackToolbar.apply {
setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.COLLAPSE) }
setOnMenuItemClickListener { item ->
if (item.itemId == R.id.action_queue) {
navModel.mainNavigateTo(MainNavigationAction.QUEUE)
true
} else {
false
}
}
setOnMenuItemClickListener(this@PlaybackPanelFragment)
queueItem = menu.findItem(R.id.action_queue)
}
@ -127,6 +120,8 @@ class PlaybackPanelFragment :
binding.playbackSkipNext.setOnClickListener { playbackModel.skipNext() }
binding.playbackShuffle.setOnClickListener { playbackModel.invertShuffled() }
binding.playbackSeekBar.apply {}
// --- VIEWMODEL SETUP --
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
@ -146,11 +141,22 @@ class PlaybackPanelFragment :
}
override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) {
binding.playbackToolbar.setOnMenuItemClickListener(null)
binding.playbackSong.isSelected = false
binding.playbackSeekBar.removeOnChangeListener(this)
binding.playbackSeekBar.removeOnChangeListener(this)
}
override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_queue -> {
navModel.mainNavigateTo(MainNavigationAction.QUEUE)
true
}
else -> false
}
}
override fun onStartTrackingTouch(slider: Slider) {
requireBinding().playbackPosition.isActivated = true
}
@ -162,7 +168,7 @@ class PlaybackPanelFragment :
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
if (fromUser) {
requireBinding().playbackPosition.textSafe = value.toLong().toDuration(true)
requireBinding().playbackPosition.textSafe = value.toLong().formatDuration(true)
}
}
@ -178,7 +184,7 @@ class PlaybackPanelFragment :
// Normally if a song had a duration
val seconds = song.seconds
binding.playbackDuration.textSafe = seconds.toDuration(false)
binding.playbackDuration.textSafe = seconds.formatDuration(false)
binding.playbackSeekBar.apply {
isEnabled = seconds > 0L
valueTo = max(seconds, 1L).toFloat()
@ -197,7 +203,7 @@ class PlaybackPanelFragment :
val binding = requireBinding()
if (!binding.playbackPosition.isActivated) {
binding.playbackSeekBar.value = position.toFloat()
binding.playbackPosition.textSafe = position.toDuration(true)
binding.playbackPosition.textSafe = position.formatDuration(true)
}
}

View file

@ -55,43 +55,35 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
private val settingsManager = SettingsManager.getInstance()
private val playbackManager = PlaybackStateManager.getInstance()
// Playback
private val mSong = MutableLiveData<Song?>()
private val mParent = MutableLiveData<MusicParent?>()
// States
private val mIsPlaying = MutableLiveData(false)
private val mPositionSecs = MutableLiveData(0L)
private val mRepeatMode = MutableLiveData(RepeatMode.NONE)
private val mIsShuffled = MutableLiveData(false)
// Queue
private val mNextUp = MutableLiveData(listOf<Song>())
// Other
private var mIntentUri: Uri? = null
private var intentUri: Uri? = null
private val _song = MutableLiveData<Song?>()
/** The current song. */
val song: LiveData<Song?>
get() = mSong
get() = _song
private val _parent = MutableLiveData<MusicParent?>()
/** The current model that is being played from, such as an [Album] or [Artist] */
val parent: LiveData<MusicParent?>
get() = mParent
get() = _parent
private val _isPlaying = MutableLiveData(false)
val isPlaying: LiveData<Boolean>
get() = mIsPlaying
get() = _isPlaying
private val _positionSecs = MutableLiveData(0L)
/** The current playback position, in seconds */
val positionSecs: LiveData<Long>
get() = mPositionSecs
get() = _positionSecs
private val _repeatMode = MutableLiveData(RepeatMode.NONE)
/** The current repeat mode, see [RepeatMode] for more information */
val repeatMode: LiveData<RepeatMode>
get() = mRepeatMode
get() = _repeatMode
private val _isShuffled = MutableLiveData(false)
val isShuffled: LiveData<Boolean>
get() = mIsShuffled
get() = _isShuffled
private val _nextUp = MutableLiveData(listOf<Song>())
/** The queue, without the previous items. */
val nextUp: LiveData<List<Song>>
get() = mNextUp
get() = _nextUp
init {
playbackManager.addCallback(this)
@ -167,7 +159,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
} else {
logD("Cant play this URI right now, waiting")
mIntentUri = uri
intentUri = uri
}
}
@ -208,7 +200,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
*/
fun removeQueueDataItem(adapterIndex: Int, apply: () -> Unit) {
val index =
adapterIndex + (playbackManager.queue.size - unlikelyToBeNull(mNextUp.value).size)
adapterIndex + (playbackManager.queue.size - unlikelyToBeNull(_nextUp.value).size)
if (index in playbackManager.queue.indices) {
apply()
playbackManager.removeQueueItem(index)
@ -219,7 +211,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
* is called just before the change is committed so that the adapter can be updated.
*/
fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int, apply: () -> Unit): Boolean {
val delta = (playbackManager.queue.size - unlikelyToBeNull(mNextUp.value).size)
val delta = (playbackManager.queue.size - unlikelyToBeNull(_nextUp.value).size)
val from = adapterFrom + delta
val to = adapterTo + delta
if (from in playbackManager.queue.indices && to in playbackManager.queue.indices) {
@ -287,12 +279,12 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
* - Restore the last playback state if there is no active file intent.
*/
fun setupPlayback(context: Context) {
val intentUri = mIntentUri
val intentUri = intentUri
if (intentUri != null) {
playWithUriInternal(intentUri, context)
// Remove the uri after finishing the calls so that this does not fire again.
mIntentUri = null
this.intentUri = null
} else if (!playbackManager.isInitialized) {
// Otherwise just restore
viewModelScope.launch { playbackManager.restoreState(context) }
@ -319,33 +311,33 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
}
override fun onIndexMoved(index: Int) {
mSong.value = playbackManager.song
mNextUp.value = playbackManager.queue.slice(index.inc() until playbackManager.queue.size)
_song.value = playbackManager.song
_nextUp.value = playbackManager.queue.slice(index + 1 until playbackManager.queue.size)
}
override fun onQueueChanged(index: Int, queue: List<Song>) {
mNextUp.value = queue.slice(index.inc() until queue.size)
_nextUp.value = queue.slice(index + 1 until queue.size)
}
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
mParent.value = playbackManager.parent
mSong.value = playbackManager.song
mNextUp.value = queue.slice(index.inc() until queue.size)
_parent.value = playbackManager.parent
_song.value = playbackManager.song
_nextUp.value = queue.slice(index + 1 until queue.size)
}
override fun onPositionChanged(positionMs: Long) {
mPositionSecs.value = positionMs / 1000
_positionSecs.value = positionMs / 1000
}
override fun onPlayingChanged(isPlaying: Boolean) {
mIsPlaying.value = isPlaying
_isPlaying.value = isPlaying
}
override fun onShuffledChanged(isShuffled: Boolean) {
mIsShuffled.value = isShuffled
_isShuffled.value = isShuffled
}
override fun onRepeatChanged(repeatMode: RepeatMode) {
mRepeatMode.value = repeatMode
_repeatMode.value = repeatMode
}
}

View file

@ -118,31 +118,31 @@ class HybridBackingData<T>(
private val adapter: RecyclerView.Adapter<*>,
diffCallback: DiffUtil.ItemCallback<T>
) : BackingData<T>() {
private var mCurrentList = mutableListOf<T>()
private var _currentList = mutableListOf<T>()
val currentList: List<T>
get() = mCurrentList
get() = _currentList
private val differ = AsyncListDiffer(adapter, diffCallback)
override fun getItem(position: Int): T = mCurrentList[position]
override fun getItemCount(): Int = mCurrentList.size
override fun getItem(position: Int): T = _currentList[position]
override fun getItemCount(): Int = _currentList.size
fun submitList(newData: List<T>, onDone: () -> Unit = {}) {
if (newData != mCurrentList) {
mCurrentList = newData.toMutableList()
if (newData != _currentList) {
_currentList = newData.toMutableList()
differ.submitList(newData, onDone)
}
}
fun moveItems(from: Int, to: Int) {
mCurrentList.add(to, mCurrentList.removeAt(from))
differ.rewriteListUnsafe(mCurrentList)
_currentList.add(to, _currentList.removeAt(from))
differ.rewriteListUnsafe(_currentList)
adapter.notifyItemMoved(from, to)
}
fun removeItem(at: Int) {
mCurrentList.removeAt(at)
differ.rewriteListUnsafe(mCurrentList)
_currentList.removeAt(at)
differ.rewriteListUnsafe(_currentList)
adapter.notifyItemRemoved(at)
}
@ -152,7 +152,7 @@ class HybridBackingData<T>(
* can do to marry the adapter primitives with DiffUtil.
*/
private fun <T> AsyncListDiffer<T>.rewriteListUnsafe(newList: List<T>) {
differMaxGenerationsField.set(this, (differMaxGenerationsField.get(this) as Int).inc())
differMaxGenerationsField.set(this, (differMaxGenerationsField.get(this) as Int) + 1)
differListField.set(this, newList.toMutableList())
differImmutableListField.set(this, newList)
}

View file

@ -41,23 +41,24 @@ import org.oxycblt.auxio.util.logD
*
* All access should be done with [PlaybackStateManager.getInstance].
* @author OxygenCobalt
*
* TODO: Add a controller role and move song loading/seeking to that TODO: Make PlaybackViewModel
* pass "delayed actions" to this and then await the service to start it???
*/
class PlaybackStateManager private constructor() {
private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance()
// Playback
private var mutableQueue = mutableListOf<Song>()
/** The currently playing song. Null if there isn't one */
val song
get() = queue.getOrNull(index)
/** The parent the queue is based on, null if all songs */
var parent: MusicParent? = null
private set
private var _queue = mutableListOf<Song>()
/** The current queue determined by [parent] */
val queue
get() = mutableQueue
get() = _queue
/** The current position in the queue */
var index = -1
private set
@ -160,8 +161,8 @@ class PlaybackStateManager private constructor() {
fun next() {
// Increment the index, if it cannot be incremented any further, then
// repeat and pause/resume playback depending on the setting
if (index < mutableQueue.lastIndex) {
goto(index.inc(), true)
if (index < _queue.lastIndex) {
goto(index + 1, true)
} else {
goto(0, repeatMode == RepeatMode.ALL)
}
@ -174,7 +175,7 @@ class PlaybackStateManager private constructor() {
rewind()
isPlaying = true
} else {
goto(max(index.dec(), 0), true)
goto(max(index - 1, 0), true)
}
}
@ -187,39 +188,39 @@ class PlaybackStateManager private constructor() {
/** Add a [song] to the top of the queue. */
fun playNext(song: Song) {
mutableQueue.add(index.inc(), song)
_queue.add(index + 1, song)
notifyQueueChanged()
}
/** Add a list of [songs] to the top of the queue. */
fun playNext(songs: List<Song>) {
mutableQueue.addAll(index.inc(), songs)
_queue.addAll(index + 1, songs)
notifyQueueChanged()
}
/** Add a [song] to the end of the queue. */
fun addToQueue(song: Song) {
mutableQueue.add(song)
_queue.add(song)
notifyQueueChanged()
}
/** Add a list of [songs] to the end of the queue. */
fun addToQueue(songs: List<Song>) {
mutableQueue.addAll(songs)
_queue.addAll(songs)
notifyQueueChanged()
}
/** Move a queue item at [from] to a position at [to]. Will ignore invalid indexes. */
fun moveQueueItem(from: Int, to: Int) {
logD("Moving item $from to position $to")
mutableQueue.add(to, mutableQueue.removeAt(from))
_queue.add(to, _queue.removeAt(from))
notifyQueueChanged()
}
/** Remove a queue item at [index]. Will ignore invalid indexes. */
fun removeQueueItem(index: Int) {
logD("Removing item ${mutableQueue[index].rawName}")
mutableQueue.removeAt(index)
logD("Removing item ${_queue[index].rawName}")
_queue.removeAt(index)
notifyQueueChanged()
}
@ -240,7 +241,7 @@ class PlaybackStateManager private constructor() {
) {
if (shuffled) {
if (regenShuffledQueue) {
mutableQueue =
_queue =
parent
.let { parent ->
when (parent) {
@ -253,15 +254,15 @@ class PlaybackStateManager private constructor() {
.toMutableList()
}
mutableQueue.shuffle()
_queue.shuffle()
if (keep != null) {
mutableQueue.add(0, mutableQueue.removeAt(mutableQueue.indexOf(keep)))
_queue.add(0, _queue.removeAt(_queue.indexOf(keep)))
}
index = 0
} else {
mutableQueue =
_queue =
parent
.let { parent ->
when (parent) {
@ -343,7 +344,7 @@ class PlaybackStateManager private constructor() {
if (state != null) {
index = state.index
parent = state.parent
mutableQueue = state.queue.toMutableList()
_queue = state.queue.toMutableList()
repeatMode = state.repeatMode
isShuffled = state.isShuffled
@ -372,7 +373,7 @@ class PlaybackStateManager private constructor() {
PlaybackStateDatabase.SavedState(
index = index,
parent = parent,
queue = mutableQueue,
queue = _queue,
positionMs = positionMs,
isShuffled = isShuffled,
repeatMode = repeatMode))

View file

@ -19,8 +19,10 @@ package org.oxycblt.auxio.search
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.widget.Toolbar
import androidx.core.view.isInvisible
import androidx.core.view.postDelayed
import androidx.core.widget.addTextChangedListener
@ -53,7 +55,10 @@ import org.oxycblt.auxio.util.requireAttached
* A [Fragment] that allows for the searching of the entire music library.
* @author OxygenCobalt
*/
class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemListener {
class SearchFragment :
ViewBindingFragment<FragmentSearchBinding>(),
MenuItemListener,
Toolbar.OnMenuItemClickListener {
// SearchViewModel is only scoped to this Fragment
private val searchModel: SearchViewModel by viewModels()
private val playbackModel: PlaybackViewModel by activityViewModels()
@ -75,15 +80,7 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
findNavController().navigateUp()
}
setOnMenuItemClickListener { item ->
if (item.itemId != R.id.submenu_filtering) {
searchModel.updateFilterModeWithId(context, item.itemId)
item.isChecked = true
true
} else {
false
}
}
setOnMenuItemClickListener(this@SearchFragment)
}
binding.searchEditText.apply {
@ -116,11 +113,25 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
}
override fun onDestroyBinding(binding: FragmentSearchBinding) {
super.onDestroyBinding(binding)
binding.searchToolbar.setOnMenuItemClickListener(null)
binding.searchRecycler.adapter = null
imm = null
}
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.submenu_filtering -> {}
else -> {
if (item.itemId != R.id.submenu_filtering) {
searchModel.updateFilterModeWithId(requireContext(), item.itemId)
item.isChecked = true
}
}
}
return true
}
override fun onItemClick(item: Item) {
when (item) {
is Song -> playbackModel.playSong(item)

View file

@ -43,30 +43,30 @@ class SearchViewModel : ViewModel() {
private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance()
private val mSearchResults = MutableLiveData(listOf<Item>())
private var mFilterMode: DisplayMode? = null
private var mLastQuery: String? = null
private val _searchResults = MutableLiveData(listOf<Item>())
private var _filterMode: DisplayMode? = null
private var lastQuery: String? = null
/** Current search results from the last [search] call. */
val searchResults: LiveData<List<Item>>
get() = mSearchResults
get() = _searchResults
val filterMode: DisplayMode?
get() = mFilterMode
get() = _filterMode
init {
mFilterMode = settingsManager.searchFilterMode
_filterMode = settingsManager.searchFilterMode
}
/**
* Use [query] to perform a search of the music library. Will push results to [searchResults].
*/
fun search(context: Context, query: String?) {
mLastQuery = query
lastQuery = query
val library = musicStore.library
if (query.isNullOrEmpty() || library == null) {
logD("No music/query, ignoring search")
mSearchResults.value = listOf()
_searchResults.value = listOf()
return
}
@ -79,48 +79,48 @@ class SearchViewModel : ViewModel() {
// Note: a filter mode of null means to not filter at all.
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ARTISTS) {
if (_filterMode == null || _filterMode == DisplayMode.SHOW_ARTISTS) {
library.artists.filterByOrNull(context, query)?.let { artists ->
results.add(Header(-1, R.string.lbl_artists))
results.addAll(sort.artists(artists))
}
}
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ALBUMS) {
if (_filterMode == null || _filterMode == DisplayMode.SHOW_ALBUMS) {
library.albums.filterByOrNull(context, query)?.let { albums ->
results.add(Header(-2, R.string.lbl_albums))
results.addAll(sort.albums(albums))
}
}
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_GENRES) {
if (_filterMode == null || _filterMode == DisplayMode.SHOW_GENRES) {
library.genres.filterByOrNull(context, query)?.let { genres ->
results.add(Header(-3, R.string.lbl_genres))
results.addAll(sort.genres(genres))
}
}
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_SONGS) {
if (_filterMode == null || _filterMode == DisplayMode.SHOW_SONGS) {
library.songs.filterByOrNull(context, query)?.let { songs ->
results.add(Header(-4, R.string.lbl_songs))
results.addAll(sort.songs(songs))
}
}
mSearchResults.value = results
_searchResults.value = results
}
}
/** Re-search the library using the last query. Will push results to [searchResults]. */
fun refresh(context: Context) {
search(context, mLastQuery)
search(context, lastQuery)
}
/**
* Update the current filter mode with a menu [id]. New value will be pushed to [filterMode].
*/
fun updateFilterModeWithId(context: Context, @IdRes id: Int) {
mFilterMode =
_filterMode =
when (id) {
R.id.option_filter_songs -> DisplayMode.SHOW_SONGS
R.id.option_filter_albums -> DisplayMode.SHOW_ALBUMS
@ -129,9 +129,9 @@ class SearchViewModel : ViewModel() {
else -> null
}
logD("Updating filter mode to $mFilterMode")
logD("Updating filter mode to $_filterMode")
settingsManager.searchFilterMode = mFilterMode
settingsManager.searchFilterMode = _filterMode
refresh(context)
}

View file

@ -32,8 +32,8 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentAboutBinding
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.music.toDuration
import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.systemBarInsetsCompat
@ -64,7 +64,8 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
homeModel.songs.observe(viewLifecycleOwner) { songs ->
binding.aboutSongCount.textSafe = getString(R.string.fmt_songs_loaded, songs.size)
binding.aboutTotalDuration.textSafe =
getString(R.string.fmt_total_duration, songs.sumOf { it.seconds }.toDuration(false))
getString(
R.string.fmt_total_duration, songs.sumOf { it.seconds }.formatDuration(false))
}
homeModel.albums.observe(viewLifecycleOwner) { albums ->

View file

@ -24,36 +24,39 @@ import org.oxycblt.auxio.music.Music
/** A ViewModel that handles complicated navigation situations. */
class NavigationViewModel : ViewModel() {
private val mMainNavigationAction = MutableLiveData<MainNavigationAction?>()
private val _mainNavigationAction = MutableLiveData<MainNavigationAction?>()
/** Flag for main fragment navigation. Intended for MainFragment use only. */
val mainNavigationAction: LiveData<MainNavigationAction?>
get() = mMainNavigationAction
get() = _mainNavigationAction
private val mExploreNavigationItem = MutableLiveData<Music?>()
/** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */
private val _exploreNavigationItem = MutableLiveData<Music?>()
/**
* Flag for navigation within the explore fragments. Observe this to coordinate navigation to an
* item's UI.
*/
val exploreNavigationItem: LiveData<Music?>
get() = mExploreNavigationItem
get() = _exploreNavigationItem
/** Notify MainFragment to navigate to the location outlined in [MainNavigationAction]. */
fun mainNavigateTo(action: MainNavigationAction) {
if (mMainNavigationAction.value != null) return
mMainNavigationAction.value = action
if (_mainNavigationAction.value != null) return
_mainNavigationAction.value = action
}
/** Mark that the main navigation process is done. */
fun finishMainNavigation() {
mMainNavigationAction.value = null
_mainNavigationAction.value = null
}
/** Navigate to an item's detail menu, whether a song/album/artist */
fun exploreNavigateTo(item: Music) {
if (mExploreNavigationItem.value != null) return
mExploreNavigationItem.value = item
if (_exploreNavigationItem.value != null) return
_exploreNavigationItem.value = item
}
/** Mark that the item navigation process is done. */
fun finishExploreNavigation() {
mExploreNavigationItem.value = null
_exploreNavigationItem.value = null
}
}

View file

@ -161,13 +161,13 @@ abstract class BackingData<T> {
* [AsyncBackingData] is not preferable due to bugs involving diffing.
*/
class PrimitiveBackingData<T>(private val adapter: RecyclerView.Adapter<*>) : BackingData<T>() {
private var mCurrentList = mutableListOf<T>()
private var _currentList = mutableListOf<T>()
/** The current list backing this adapter. */
val currentList: List<T>
get() = mCurrentList
get() = _currentList
override fun getItem(position: Int): T = mCurrentList[position]
override fun getItemCount(): Int = mCurrentList.size
override fun getItem(position: Int): T = _currentList[position]
override fun getItemCount(): Int = _currentList.size
/**
* Update the list with a [newList]. This calls [RecyclerView.Adapter.notifyDataSetChanged]
@ -175,7 +175,7 @@ class PrimitiveBackingData<T>(private val adapter: RecyclerView.Adapter<*>) : Ba
*/
@Suppress("NotifyDatasetChanged")
fun submitList(newList: List<T>) {
mCurrentList = newList.toMutableList()
_currentList = newList.toMutableList()
adapter.notifyDataSetChanged()
}
}

View file

@ -276,10 +276,10 @@ sealed class Sort(open val isAscending: Boolean) {
* a non-equal result being propagated upwards.
*/
class MultiComparator<T>(vararg comparators: Comparator<T>) : Comparator<T> {
private val mComparators = comparators
private val _comparators = comparators
override fun compare(a: T?, b: T?): Int {
for (comparator in mComparators) {
for (comparator in _comparators) {
val result = comparator.compare(a, b)
if (result != 0) {
return result

View file

@ -29,7 +29,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.oxycblt.auxio.util.logD
abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() {
private var mBinding: T? = null
private var _binding: T? = null
protected abstract fun onCreateBinding(inflater: LayoutInflater): T
protected open fun onBindingCreated(binding: T, savedInstanceState: Bundle?) {}
@ -37,10 +37,10 @@ abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() {
protected open fun onConfigDialog(builder: AlertDialog.Builder) {}
protected val binding: T?
get() = mBinding
get() = _binding
protected fun requireBinding(): T {
return requireNotNull(mBinding) {
return requireNotNull(_binding) {
"ViewBinding was not available, as the fragment was not in a valid state"
}
}
@ -49,7 +49,7 @@ abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = onCreateBinding(inflater).also { mBinding = it }.root
): View = onCreateBinding(inflater).also { _binding = it }.root
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireActivity(), theme).run {
@ -68,6 +68,6 @@ abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() {
override fun onDestroyView() {
super.onDestroyView()
onDestroyBinding(requireBinding())
mBinding = null
_binding = null
}
}

View file

@ -27,17 +27,17 @@ import org.oxycblt.auxio.util.logD
/** A fragment enabling ViewBinding inflation and usage across the fragment lifecycle. */
abstract class ViewBindingFragment<T : ViewBinding> : Fragment() {
private var mBinding: T? = null
private var _binding: T? = null
protected abstract fun onCreateBinding(inflater: LayoutInflater): T
protected open fun onBindingCreated(binding: T, savedInstanceState: Bundle?) {}
protected open fun onDestroyBinding(binding: T) {}
protected val binding: T?
get() = mBinding
get() = _binding
protected fun requireBinding(): T {
return requireNotNull(mBinding) {
return requireNotNull(_binding) {
"ViewBinding was not available, as the fragment was not in a valid state"
}
}
@ -46,7 +46,7 @@ abstract class ViewBindingFragment<T : ViewBinding> : Fragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = onCreateBinding(inflater).also { mBinding = it }.root
): View = onCreateBinding(inflater).also { _binding = it }.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -57,6 +57,6 @@ abstract class ViewBindingFragment<T : ViewBinding> : Fragment() {
override fun onDestroyView() {
super.onDestroyView()
onDestroyBinding(requireBinding())
mBinding = null
_binding = null
}
}

View file

@ -18,6 +18,7 @@
package org.oxycblt.auxio.util
import android.os.Looper
import android.text.format.DateUtils
import androidx.core.math.MathUtils
import org.oxycblt.auxio.BuildConfig
@ -45,3 +46,24 @@ fun Int.clamp(min: Int, max: Int): Int = MathUtils.clamp(this, min, max)
/** Shortcut to clamp an integer between [min] and [max] */
fun Long.clamp(min: Long, max: Long): Long = MathUtils.clamp(this, min, max)
/**
* Convert a [Long] of seconds into a string duration.
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
* will be returned if the second value is 0.
*/
fun Long.formatDuration(isElapsed: Boolean): String {
if (!isElapsed && this == 0L) {
logD("Non-elapsed duration is zero, using --:--")
return "--:--"
}
var durationString = DateUtils.formatElapsedTime(this)
// If the duration begins with a excess zero [e.g 01:42], then cut it off.
if (durationString[0] == '0') {
durationString = durationString.slice(1 until durationString.length)
}
return durationString
}

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Note: Not actually transparent, making it transparent would actually make it translucent -->
<color name="chrome_transparent">#01151515</color>
<color name="red_primary">#FFB4A8</color>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Note: Not actually transparent, making it transparent would actually make it translucent -->
<color name="chrome_transparent">#01fafafa</color>
<color name="chrome_translucent">#80000000</color>

View file

@ -9,7 +9,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.3'
classpath 'com.android.tools.build:gradle:7.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version"
classpath "com.diffplug.spotless:spotless-plugin-gradle:6.3.0"