diff --git a/app/build.gradle b/app/build.gradle index c9770f51e..309abbf8a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index a3b314362..1fb87fe9f 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/coil/BaseFetcher.kt b/app/src/main/java/org/oxycblt/auxio/coil/BaseFetcher.kt index b2fa042f9..7370e59de 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/BaseFetcher.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/BaseFetcher.kt @@ -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 } } diff --git a/app/src/main/java/org/oxycblt/auxio/coil/Components.kt b/app/src/main/java/org/oxycblt/auxio/coil/Components.kt index 6992d4997..d2c2e0da7 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/Components.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/Components.kt @@ -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) } diff --git a/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt b/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt index 8b347d8b9..6895cc657 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt @@ -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 Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true) } return dst diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index debd0f381..2e7264922 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -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) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index f813dfc83..534a5b818 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -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) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt index aed147bf6..80123b514 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt @@ -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(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 } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt index 4309a0e9a..890e48e24 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -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() { +abstract class DetailFragment : + ViewBindingFragment(), 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() { override fun onDestroyBinding(binding: FragmentDetailBinding) { super.onDestroyBinding(binding) + binding.detailToolbar.setOnMenuItemClickListener(null) binding.detailRecycler.adapter = null } @@ -56,13 +59,8 @@ abstract class DetailFragment : ViewBindingFragment() { * 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() { } setNavigationOnClickListener { findNavController().navigateUp() } - - onMenuClick?.let { onClick -> - setOnMenuItemClickListener { item -> onClick(item.itemId) } - } + setOnMenuItemClickListener(this@DetailFragment) } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 5e2c8d344..4642f06eb 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -44,13 +44,13 @@ class DetailViewModel : ViewModel() { private val musicStore = MusicStore.getInstance() private val settingsManager = SettingsManager.getInstance() - private val mCurrentAlbum = MutableLiveData() + private val _currentAlbum = MutableLiveData() val currentAlbum: LiveData - get() = mCurrentAlbum + get() = _currentAlbum - private val mAlbumData = MutableLiveData(listOf()) + private val _albumData = MutableLiveData(listOf()) val albumData: LiveData> - 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() + private val _currentArtist = MutableLiveData() val currentArtist: LiveData - get() = mCurrentArtist + get() = _currentArtist - private val mArtistData = MutableLiveData(listOf()) - val artistData: LiveData> = mArtistData + private val _artistData = MutableLiveData(listOf()) + val artistData: LiveData> = _artistData var artistSort: Sort get() = settingsManager.detailArtistSort @@ -73,12 +73,12 @@ class DetailViewModel : ViewModel() { currentArtist.value?.let(::refreshArtistData) } - private val mCurrentGenre = MutableLiveData() + private val _currentGenre = MutableLiveData() val currentGenre: LiveData - get() = mCurrentGenre + get() = _currentGenre - private val mGenreData = MutableLiveData(listOf()) - val genreData: LiveData> = mGenreData + private val _genreData = MutableLiveData(listOf()) + val genreData: LiveData> = _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(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(album) data.add(SortHeader(id = -2, R.string.lbl_songs)) data.addAll(albumSort.album(album)) - mAlbumData.value = data + _albumData.value = data } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index f92b6361d..404dffdb2 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -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) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt index b97c9f37f..17025ba53 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt @@ -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) } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index 330477811..27ab09d8e 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -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() { +class HomeFragment : ViewBindingFragment(), 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() { 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() { // --- 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() { 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() { .assignId(item.itemId))) } } + + return true } private fun updateFastScrolling(isFastScrolling: Boolean) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index d15ee4cba..102a20093 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -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()) + private val _songs = MutableLiveData(listOf()) val songs: LiveData> - get() = mSongs + get() = _songs - private val mAlbums = MutableLiveData(listOf()) + private val _albums = MutableLiveData(listOf()) val albums: LiveData> - get() = mAlbums + get() = _albums - private val mArtists = MutableLiveData(listOf()) + private val _artists = MutableLiveData(listOf()) val artists: LiveData> - get() = mArtists + get() = _artists - private val mGenres = MutableLiveData(listOf()) + private val _genres = MutableLiveData(listOf()) val genres: LiveData> - get() = mGenres + get() = _genres var tabs: List = visibleTabs private set @@ -63,18 +63,18 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback private val visibleTabs: List get() = settingsManager.libTabs.filterIsInstance().map { it.mode } - private val mCurrentTab = MutableLiveData(tabs[0]) - val currentTab: LiveData = mCurrentTab + private val _currentTab = MutableLiveData(tabs[0]) + val currentTab: LiveData = _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 = mRecreateTabs + private val _shouldRecreateTabs = MutableLiveData(false) + val recreateTabs: LiveData = _shouldRecreateTabs - private val mFastScrolling = MutableLiveData(false) - val fastScrolling: LiveData = mFastScrolling + private val _isFastScrolling = MutableLiveData(false) + val isFastScrolling: LiveData = _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) { tabs = visibleTabs - mRecreateTabs.value = true + _shouldRecreateTabs.value = true } override fun onCleared() { diff --git a/app/src/main/java/org/oxycblt/auxio/music/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/Indexer.kt index bf12b5485..f574a7336 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Indexer.kt @@ -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): List { val albums = mutableListOf() - 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): List { val artists = mutableListOf() - 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) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 2de01acc9..515ac37ff 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -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, /** 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) : 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) : 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) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt index 82ae473a3..2fc2e2dbb 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -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() diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt index 0be39f9d8..0d6d2f9f1 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt @@ -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 -} diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt index 4b391d2af..ee0c8a701 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -28,8 +28,8 @@ import org.oxycblt.auxio.util.logD class MusicViewModel : ViewModel(), MusicStore.Callback { private val musicStore = MusicStore.getInstance() - private val mLoaderResponse = MutableLiveData(null) - val loaderResponse: LiveData = mLoaderResponse + private val _loaderResponse = MutableLiveData(null) + val loaderResponse: LiveData = _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() { diff --git a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDatabase.kt index a8eede553..56171f687 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDatabase.kt @@ -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) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt index 2fc7e7d2b..3e400cf3c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt @@ -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()) + private val _paths = MutableLiveData(mutableListOf()) val paths: LiveData> - 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") diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index c85d749d6..036a42be0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -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(), 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) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 1b7fc5e8a..61e314b22 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -55,43 +55,35 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { private val settingsManager = SettingsManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance() - // Playback - private val mSong = MutableLiveData() - private val mParent = MutableLiveData() - - // 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()) - - // Other - private var mIntentUri: Uri? = null + private var intentUri: Uri? = null + private val _song = MutableLiveData() /** The current song. */ val song: LiveData - get() = mSong + get() = _song + private val _parent = MutableLiveData() /** The current model that is being played from, such as an [Album] or [Artist] */ val parent: LiveData - get() = mParent - + get() = _parent + private val _isPlaying = MutableLiveData(false) val isPlaying: LiveData - get() = mIsPlaying + get() = _isPlaying + private val _positionSecs = MutableLiveData(0L) /** The current playback position, in seconds */ val positionSecs: LiveData - get() = mPositionSecs + get() = _positionSecs + private val _repeatMode = MutableLiveData(RepeatMode.NONE) /** The current repeat mode, see [RepeatMode] for more information */ val repeatMode: LiveData - get() = mRepeatMode + get() = _repeatMode + private val _isShuffled = MutableLiveData(false) val isShuffled: LiveData - get() = mIsShuffled + get() = _isShuffled + private val _nextUp = MutableLiveData(listOf()) /** The queue, without the previous items. */ val nextUp: LiveData> - 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) { - 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, 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 } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt index dc5d92498..3f22551fb 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt @@ -118,31 +118,31 @@ class HybridBackingData( private val adapter: RecyclerView.Adapter<*>, diffCallback: DiffUtil.ItemCallback ) : BackingData() { - private var mCurrentList = mutableListOf() + private var _currentList = mutableListOf() val currentList: List - 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, 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( * can do to marry the adapter primitives with DiffUtil. */ private fun AsyncListDiffer.rewriteListUnsafe(newList: List) { - 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) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 685022df0..e7016b7af 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -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() - /** 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() /** 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) { - 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) { - 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)) diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index 9a1a486c3..1b644b54e 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -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(), MenuItemListener { +class SearchFragment : + ViewBindingFragment(), + 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(), 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(), 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) diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index 6e733636d..dcb78c8fe 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -43,30 +43,30 @@ class SearchViewModel : ViewModel() { private val musicStore = MusicStore.getInstance() private val settingsManager = SettingsManager.getInstance() - private val mSearchResults = MutableLiveData(listOf()) - private var mFilterMode: DisplayMode? = null - private var mLastQuery: String? = null + private val _searchResults = MutableLiveData(listOf()) + private var _filterMode: DisplayMode? = null + private var lastQuery: String? = null /** Current search results from the last [search] call. */ val searchResults: LiveData> - 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) } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt index 7e52c3bf6..d50de6a02 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt @@ -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() { 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 -> diff --git a/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt b/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt index 6c71f6e20..45a6ae47a 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt @@ -24,36 +24,39 @@ import org.oxycblt.auxio.music.Music /** A ViewModel that handles complicated navigation situations. */ class NavigationViewModel : ViewModel() { - private val mMainNavigationAction = MutableLiveData() + private val _mainNavigationAction = MutableLiveData() /** Flag for main fragment navigation. Intended for MainFragment use only. */ val mainNavigationAction: LiveData - get() = mMainNavigationAction + get() = _mainNavigationAction - private val mExploreNavigationItem = MutableLiveData() - /** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */ + private val _exploreNavigationItem = MutableLiveData() + /** + * Flag for navigation within the explore fragments. Observe this to coordinate navigation to an + * item's UI. + */ val exploreNavigationItem: LiveData - 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 } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt b/app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt index 69dd20b20..5f1c15531 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt @@ -161,13 +161,13 @@ abstract class BackingData { * [AsyncBackingData] is not preferable due to bugs involving diffing. */ class PrimitiveBackingData(private val adapter: RecyclerView.Adapter<*>) : BackingData() { - private var mCurrentList = mutableListOf() + private var _currentList = mutableListOf() /** The current list backing this adapter. */ val currentList: List - 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(private val adapter: RecyclerView.Adapter<*>) : Ba */ @Suppress("NotifyDatasetChanged") fun submitList(newList: List) { - mCurrentList = newList.toMutableList() + _currentList = newList.toMutableList() adapter.notifyDataSetChanged() } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt b/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt index d11e4325a..828fddf3d 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt @@ -276,10 +276,10 @@ sealed class Sort(open val isAscending: Boolean) { * a non-equal result being propagated upwards. */ class MultiComparator(vararg comparators: Comparator) : Comparator { - 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 diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt index 85fb0bcd1..d54e3a53d 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt @@ -29,7 +29,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.oxycblt.auxio.util.logD abstract class ViewBindingDialogFragment : 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 : 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 : 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 : DialogFragment() { override fun onDestroyView() { super.onDestroyView() onDestroyBinding(requireBinding()) - mBinding = null + _binding = null } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt index 00d0a0d8d..d2e932c3f 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt @@ -27,17 +27,17 @@ import org.oxycblt.auxio.util.logD /** A fragment enabling ViewBinding inflation and usage across the fragment lifecycle. */ abstract class ViewBindingFragment : 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 : 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 : Fragment() { override fun onDestroyView() { super.onDestroyView() onDestroyBinding(requireBinding()) - mBinding = null + _binding = null } } diff --git a/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt index 11e71826e..6e1158e1a 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt @@ -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 +} diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 903992c11..7d2d8e6a3 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -1,5 +1,6 @@ + #01151515 #FFB4A8 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index c67eb7175..a65c80ea7 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,5 +1,6 @@ + #01fafafa #80000000 diff --git a/build.gradle b/build.gradle index 5489e4e00..267729ba9 100644 --- a/build.gradle +++ b/build.gradle @@ -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"