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 { android {
compileSdkVersion 32
buildToolsVersion "32.0.0"
defaultConfig { defaultConfig {
applicationId "org.oxycblt.auxio" applicationId "org.oxycblt.auxio"
versionName "2.2.2" 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 { buildTypes {
debug { debug {
debuggable true debuggable true
@ -35,17 +46,6 @@ android {
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 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 { afterEvaluate {
@ -102,7 +102,7 @@ dependencies {
implementation "io.coil-kt:coil:2.0.0-rc03" implementation "io.coil-kt:coil:2.0.0-rc03"
// Material // Material
implementation "com.google.android.material:material:1.6.0-rc01" implementation "com.google.android.material:material:1.6.0"
// LeakCanary // LeakCanary
debugImplementation "com.squareup.leakcanary:leakcanary-android:2.9.1" 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: 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: Fix how selection works in the RecyclerViews (doing it poorly right now)
* *
* TODO: Rework padding ethos * TODO: Rework padding ethos

View file

@ -275,6 +275,6 @@ abstract class BaseFetcher : Fetcher {
private fun Dimension.mosaicSize(): Int { private fun Dimension.mosaicSize(): Int {
val size = pxOrElse { 512 } 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, private val genre: Genre,
) : BaseFetcher() { ) : BaseFetcher() {
override suspend fun fetch(): FetchResult? { 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 albums = genre.songs.groupBy { it.album }.keys
val results = albums.mapAtMost(4) { album -> fetchArt(context, album) } 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.size.pxOrElse
import coil.transform.Transformation import coil.transform.Transformation
import kotlin.math.min 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 * 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) val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
if (dstSize != desiredWidth || dstSize != desiredHeight) { if (dstSize != desiredWidth || dstSize != desiredHeight) {
try { return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
// 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 return dst

View file

@ -19,6 +19,7 @@ package org.oxycblt.auxio.detail
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.core.view.children import androidx.core.view.children
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
@ -54,23 +55,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
detailModel.setAlbumId(args.albumId) detailModel.setAlbumId(args.albumId)
setupToolbar(unlikelyToBeNull(detailModel.currentAlbum.value), R.menu.menu_album_detail) { 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
}
}
requireBinding().detailRecycler.apply { requireBinding().detailRecycler.apply {
adapter = detailAdapter adapter = detailAdapter
applySpans { pos -> applySpans { pos ->
@ -86,6 +71,22 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
playbackModel.song.observe(viewLifecycleOwner, ::updateSong) 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) { override fun onItemClick(item: Item) {
if (item is Song) { if (item is Song) {
playbackModel.playSong(item, PlaybackMode.IN_ALBUM) playbackModel.playSong(item, PlaybackMode.IN_ALBUM)

View file

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

View file

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

View file

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

View file

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

View file

@ -18,6 +18,7 @@
package org.oxycblt.auxio.detail package org.oxycblt.auxio.detail
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
@ -65,6 +66,8 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
playbackModel.song.observe(viewLifecycleOwner, ::updateSong) playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
} }
override fun onMenuItemClick(item: MenuItem): Boolean = false
override fun onItemClick(item: Item) { override fun onItemClick(item: Item) {
when (item) { when (item) {
is Song -> playbackModel.playSong(item, PlaybackMode.IN_GENRE) 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.databinding.ItemDetailBinding
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.toDuration
import org.oxycblt.auxio.ui.BindingViewHolder import org.oxycblt.auxio.ui.BindingViewHolder
import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuItemListener import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.SimpleItemCallback import org.oxycblt.auxio.ui.SimpleItemCallback
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.getPluralSafe import org.oxycblt.auxio.util.getPluralSafe
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.textSafe 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.songName.textSafe = item.resolveName(binding.context)
binding.songDuration.textSafe = item.seconds.toDuration(false) binding.songDuration.textSafe = item.seconds.formatDuration(false)
binding.root.apply { binding.root.apply {
setOnClickListener { listener.onItemClick(item) } setOnClickListener { listener.onItemClick(item) }

View file

@ -20,6 +20,7 @@ package org.oxycblt.auxio.home
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.widget.Toolbar
import androidx.core.view.iterator import androidx.core.view.iterator
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
@ -60,7 +61,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* *
* TODO: Add duration and song count sorts * 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 playbackModel: PlaybackViewModel by activityViewModels()
private val navModel: NavigationViewModel by activityViewModels() private val navModel: NavigationViewModel by activityViewModels()
private val homeModel: HomeViewModel by activityViewModels() private val homeModel: HomeViewModel by activityViewModels()
@ -73,11 +74,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
binding.homeToolbar.apply { binding.homeToolbar.apply {
sortItem = menu.findItem(R.id.submenu_sorting) sortItem = menu.findItem(R.id.submenu_sorting)
setOnMenuItemClickListener(this@HomeFragment)
setOnMenuItemClickListener { item ->
onMenuClick(item)
true
}
} }
binding.homePager.apply { binding.homePager.apply {
@ -103,7 +100,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
homeModel.fastScrolling.observe(viewLifecycleOwner, ::updateFastScrolling) homeModel.isFastScrolling.observe(viewLifecycleOwner, ::updateFastScrolling)
homeModel.currentTab.observe(viewLifecycleOwner) { tab -> updateCurrentTab(sortItem, tab) } homeModel.currentTab.observe(viewLifecycleOwner) { tab -> updateCurrentTab(sortItem, tab) }
homeModel.recreateTabs.observe(viewLifecycleOwner, ::handleRecreateTabs) homeModel.recreateTabs.observe(viewLifecycleOwner, ::handleRecreateTabs)
@ -111,7 +108,12 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation) 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) { when (item.itemId) {
R.id.action_search -> { R.id.action_search -> {
logD("Navigating to search") logD("Navigating to search")
@ -147,6 +149,8 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
.assignId(item.itemId))) .assignId(item.itemId)))
} }
} }
return true
} }
private fun updateFastScrolling(isFastScrolling: Boolean) { 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 musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
private val mSongs = MutableLiveData(listOf<Song>()) private val _songs = MutableLiveData(listOf<Song>())
val songs: LiveData<List<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>> 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>> 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>> val genres: LiveData<List<Genre>>
get() = mGenres get() = _genres
var tabs: List<DisplayMode> = visibleTabs var tabs: List<DisplayMode> = visibleTabs
private set private set
@ -63,18 +63,18 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
private val visibleTabs: List<DisplayMode> private val visibleTabs: List<DisplayMode>
get() = settingsManager.libTabs.filterIsInstance<Tab.Visible>().map { it.mode } get() = settingsManager.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
private val mCurrentTab = MutableLiveData(tabs[0]) private val _currentTab = MutableLiveData(tabs[0])
val currentTab: LiveData<DisplayMode> = mCurrentTab val currentTab: LiveData<DisplayMode> = _currentTab
/** /**
* Marker to recreate all library tabs, usually initiated by a settings change. When this flag * 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. * is set, all tabs (and their respective viewpager fragments) will be recreated from scratch.
*/ */
private val mRecreateTabs = MutableLiveData(false) private val _shouldRecreateTabs = MutableLiveData(false)
val recreateTabs: LiveData<Boolean> = mRecreateTabs val recreateTabs: LiveData<Boolean> = _shouldRecreateTabs
private val mFastScrolling = MutableLiveData(false) private val _isFastScrolling = MutableLiveData(false)
val fastScrolling: LiveData<Boolean> = mFastScrolling val isFastScrolling: LiveData<Boolean> = _isFastScrolling
init { init {
musicStore.addCallback(this) 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. */ /** Update the current tab based off of the new ViewPager position. */
fun updateCurrentTab(pos: Int) { fun updateCurrentTab(pos: Int) {
logD("Updating current tab to ${tabs[pos]}") logD("Updating current tab to ${tabs[pos]}")
mCurrentTab.value = tabs[pos] _currentTab.value = tabs[pos]
} }
fun finishRecreateTabs() { fun finishRecreateTabs() {
mRecreateTabs.value = false _shouldRecreateTabs.value = false
} }
fun getSortForDisplay(displayMode: DisplayMode): Sort { fun getSortForDisplay(displayMode: DisplayMode): Sort {
@ -102,23 +102,23 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
/** Update the currently displayed item's [Sort]. */ /** Update the currently displayed item's [Sort]. */
fun updateCurrentSort(sort: Sort) { fun updateCurrentSort(sort: Sort) {
logD("Updating ${mCurrentTab.value} sort to $sort") logD("Updating ${_currentTab.value} sort to $sort")
when (mCurrentTab.value) { when (_currentTab.value) {
DisplayMode.SHOW_SONGS -> { DisplayMode.SHOW_SONGS -> {
settingsManager.libSongSort = sort settingsManager.libSongSort = sort
mSongs.value = sort.songs(unlikelyToBeNull(mSongs.value)) _songs.value = sort.songs(unlikelyToBeNull(_songs.value))
} }
DisplayMode.SHOW_ALBUMS -> { DisplayMode.SHOW_ALBUMS -> {
settingsManager.libAlbumSort = sort settingsManager.libAlbumSort = sort
mAlbums.value = sort.albums(unlikelyToBeNull(mAlbums.value)) _albums.value = sort.albums(unlikelyToBeNull(_albums.value))
} }
DisplayMode.SHOW_ARTISTS -> { DisplayMode.SHOW_ARTISTS -> {
settingsManager.libArtistSort = sort settingsManager.libArtistSort = sort
mArtists.value = sort.artists(unlikelyToBeNull(mArtists.value)) _artists.value = sort.artists(unlikelyToBeNull(_artists.value))
} }
DisplayMode.SHOW_GENRES -> { DisplayMode.SHOW_GENRES -> {
settingsManager.libGenreSort = sort settingsManager.libGenreSort = sort
mGenres.value = sort.genres(unlikelyToBeNull(mGenres.value)) _genres.value = sort.genres(unlikelyToBeNull(_genres.value))
} }
else -> {} else -> {}
} }
@ -129,7 +129,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
* begins to fast scroll. * begins to fast scroll.
*/ */
fun updateFastScrolling(scrolling: Boolean) { fun updateFastScrolling(scrolling: Boolean) {
mFastScrolling.value = scrolling _isFastScrolling.value = scrolling
} }
// --- OVERRIDES --- // --- OVERRIDES ---
@ -137,16 +137,16 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
override fun onMusicUpdate(response: MusicStore.Response) { override fun onMusicUpdate(response: MusicStore.Response) {
if (response is MusicStore.Response.Ok) { if (response is MusicStore.Response.Ok) {
val library = response.library val library = response.library
mSongs.value = settingsManager.libSongSort.songs(library.songs) _songs.value = settingsManager.libSongSort.songs(library.songs)
mAlbums.value = settingsManager.libAlbumSort.albums(library.albums) _albums.value = settingsManager.libAlbumSort.albums(library.albums)
mArtists.value = settingsManager.libArtistSort.artists(library.artists) _artists.value = settingsManager.libArtistSort.artists(library.artists)
mGenres.value = settingsManager.libGenreSort.genres(library.genres) _genres.value = settingsManager.libGenreSort.genres(library.genres)
} }
} }
override fun onLibTabsUpdate(libTabs: Array<Tab>) { override fun onLibTabsUpdate(libTabs: Array<Tab>) {
tabs = visibleTabs tabs = visibleTabs
mRecreateTabs.value = true _shouldRecreateTabs.value = true
} }
override fun onCleared() { override fun onCleared() {

View file

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

View file

@ -24,6 +24,7 @@ import android.provider.MediaStore
import androidx.core.text.isDigitsOnly import androidx.core.text.isDigitsOnly
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
// --- MUSIC MODELS --- // --- MUSIC MODELS ---
@ -59,17 +60,17 @@ data class Song(
/** The track number of this song, null if there isn't any. */ /** The track number of this song, null if there isn't any. */
val track: Int?, val track: Int?,
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val internalMediaStoreId: Long, val _mediaStoreId: Long,
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val internalMediaStoreYear: Int?, val _mediaStoreYear: Int?,
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val internalMediaStoreAlbumName: String, val _mediaStoreAlbumName: String,
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val internalMediaStoreAlbumId: Long, val _mediaStoreAlbumId: Long,
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val internalMediaStoreArtistName: String?, val _mediaStoreArtistName: String?,
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val internalMediaStoreAlbumArtistName: String?, val _mediaStoreAlbumArtistName: String?,
) : Music() { ) : Music() {
override val id: Long override val id: Long
get() { get() {
@ -89,68 +90,65 @@ data class Song(
/** The URI for this song. */ /** The URI for this song. */
val uri: Uri val uri: Uri
get() = get() =
ContentUris.withAppendedId( ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, _mediaStoreId)
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, internalMediaStoreId)
/** The duration of this song, in seconds (rounded down) */ /** The duration of this song, in seconds (rounded down) */
val seconds: Long val seconds: Long
get() = duration / 1000 get() = duration / 1000
private var mAlbum: Album? = null private var _album: Album? = null
/** The album of this song. */ /** The album of this song. */
val album: Album 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. */ /** The genre of this song. Will be an "unknown genre" if the song does not have any. */
val genre: Genre 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 * 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. * back to the album artist tag (i.e parent artist name). Null if name is unknown.
*/ */
val individualRawArtistName: String? 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 * 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) * falls back to the album artist tag (i.e parent artist name)
*/ */
fun resolveIndividualArtistName(context: Context) = fun resolveIndividualArtistName(context: Context) =
internalMediaStoreArtistName ?: album.artist.resolveName(context) _mediaStoreArtistName ?: album.artist.resolveName(context)
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val internalAlbumGroupingId: Long val _albumGroupingId: Long
get() { get() {
var result = internalGroupingArtistName.lowercase().hashCode().toLong() var result = _artistGroupingName.lowercase().hashCode().toLong()
result = 31 * result + internalMediaStoreAlbumName.lowercase().hashCode() result = 31 * result + _mediaStoreAlbumName.lowercase().hashCode()
return result return result
} }
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val internalGroupingArtistName: String val _artistGroupingName: String
get() = get() = _mediaStoreAlbumArtistName ?: _mediaStoreArtistName ?: MediaStore.UNKNOWN_STRING
internalMediaStoreAlbumArtistName
?: internalMediaStoreArtistName ?: MediaStore.UNKNOWN_STRING
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val internalIsMissingAlbum: Boolean val _isMissingAlbum: Boolean
get() = mAlbum == null get() = _album == null
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val internalIsMissingArtist: Boolean val _isMissingArtist: Boolean
get() = mAlbum?.internalIsMissingArtist ?: true get() = _album?._isMissingArtist ?: true
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val internalIsMissingGenre: Boolean val _isMissingGenre: Boolean
get() = mGenre == null get() = _genre == null
/** Internal method. Do not use. */ /** Internal method. Do not use. */
fun internalLinkAlbum(album: Album) { fun _linkAlbum(album: Album) {
mAlbum = album _album = album
} }
/** Internal method. Do not use. */ /** Internal method. Do not use. */
fun internalLinkGenre(genre: Genre) { fun _linkGenre(genre: Genre) {
mGenre = genre _genre = genre
} }
} }
@ -164,11 +162,11 @@ data class Album(
/** The songs of this album. */ /** The songs of this album. */
val songs: List<Song>, val songs: List<Song>,
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val internalGroupingArtistName: String, val _artistGroupingName: String,
) : MusicParent() { ) : MusicParent() {
init { init {
for (song in songs) { for (song in songs) {
song.internalLinkAlbum(this) song._linkAlbum(this)
} }
} }
@ -187,24 +185,24 @@ data class Album(
/** The formatted total duration of this album */ /** The formatted total duration of this album */
val totalDuration: String 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. */ /** The parent artist of this album. */
val artist: Artist val artist: Artist
get() = unlikelyToBeNull(mArtist) get() = unlikelyToBeNull(_artist)
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val internalArtistGroupingId: Long val _artistGroupingId: Long
get() = internalGroupingArtistName.lowercase().hashCode().toLong() get() = _artistGroupingName.lowercase().hashCode().toLong()
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val internalIsMissingArtist: Boolean val _isMissingArtist: Boolean
get() = mArtist == null get() = _artist == null
/** Internal method. Do not use. */ /** Internal method. Do not use. */
fun internalLinkArtist(artist: Artist) { fun _linkArtist(artist: Artist) {
mArtist = artist _artist = artist
} }
} }
@ -219,7 +217,7 @@ data class Artist(
) : MusicParent() { ) : MusicParent() {
init { init {
for (album in albums) { 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() { data class Genre(override val rawName: String?, val songs: List<Song>) : MusicParent() {
init { init {
for (song in songs) { 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 */ /** The formatted total duration of this genre */
val totalDuration: String 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. */ /** 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") 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) { for (callback in callbacks) {
callback.onMusicUpdate(newResponse) callback.onMusicUpdate(newResponse)
} }
return newResponse return newResponse
} }
private fun indexImpl(context: Context): Response { private fun loadImpl(context: Context): Response {
val notGranted = val notGranted =
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_DENIED PackageManager.PERMISSION_DENIED
@ -81,7 +81,7 @@ class MusicStore private constructor() {
val response = val response =
try { try {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
val library = Indexer.run(context) val library = Indexer.index(context)
if (library != null) { if (library != null) {
logD( logD(
"Music load completed successfully in ${System.currentTimeMillis() - start}ms") "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 * A response that [MusicStore] returns when loading music. And before you ask, yes, I do like
* rust. * rust.
*
* TODO: Add the exception to the "FAILED" ErrorKind
*/ */
sealed class Response { sealed class Response {
class Ok(val library: Library) : Response() class Ok(val library: Library) : Response()

View file

@ -17,28 +17,9 @@
package org.oxycblt.auxio.music package org.oxycblt.auxio.music
import android.text.format.DateUtils
import org.oxycblt.auxio.util.logD
// --- EXTENSION FUNCTIONS --- // --- 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 { class MusicViewModel : ViewModel(), MusicStore.Callback {
private val musicStore = MusicStore.getInstance() private val musicStore = MusicStore.getInstance()
private val mLoaderResponse = MutableLiveData<MusicStore.Response?>(null) private val _loaderResponse = MutableLiveData<MusicStore.Response?>(null)
val loaderResponse: LiveData<MusicStore.Response?> = mLoaderResponse val loaderResponse: LiveData<MusicStore.Response?> = _loaderResponse
private var isBusy = false private var isBusy = false
@ -42,29 +42,29 @@ class MusicViewModel : ViewModel(), MusicStore.Callback {
* navigated to and because SnackBars will have the best UX here. * navigated to and because SnackBars will have the best UX here.
*/ */
fun loadMusic(context: Context) { fun loadMusic(context: Context) {
if (mLoaderResponse.value != null || isBusy) { if (_loaderResponse.value != null || isBusy) {
logD("Loader is busy/already completed, not reloading") logD("Loader is busy/already completed, not reloading")
return return
} }
isBusy = true isBusy = true
mLoaderResponse.value = null _loaderResponse.value = null
viewModelScope.launch { viewModelScope.launch {
val result = musicStore.index(context) val result = musicStore.load(context)
mLoaderResponse.value = result _loaderResponse.value = result
isBusy = false isBusy = false
} }
} }
fun reloadMusic(context: Context) { fun reloadMusic(context: Context) {
logD("Reloading music library") logD("Reloading music library")
mLoaderResponse.value = null _loaderResponse.value = null
loadMusic(context) loadMusic(context)
} }
override fun onMusicUpdate(response: MusicStore.Response) { override fun onMusicUpdate(response: MusicStore.Response) {
mLoaderResponse.value = response _loaderResponse.value = response
} }
override fun onCleared() { 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 * 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 * 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 * @author OxygenCobalt
*/ */
class ExcludedDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) { 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 * TODO: Unify with MusicViewModel
*/ */
class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewModel() { class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewModel() {
private val mPaths = MutableLiveData(mutableListOf<String>()) private val _paths = MutableLiveData(mutableListOf<String>())
val paths: LiveData<MutableList<String>> val paths: LiveData<MutableList<String>>
get() = mPaths get() = _paths
var isModified: Boolean = false var isModified: Boolean = false
private set private set
@ -53,10 +53,10 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
* called. * called.
*/ */
fun addPath(path: String) { fun addPath(path: String) {
val paths = unlikelyToBeNull(mPaths.value) val paths = unlikelyToBeNull(_paths.value)
if (!paths.contains(path)) { if (!paths.contains(path)) {
paths.add(path) paths.add(path)
mPaths.value = mPaths.value _paths.value = _paths.value
isModified = true isModified = true
} }
} }
@ -66,8 +66,8 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
* [save] is called. * [save] is called.
*/ */
fun removePath(path: String) { fun removePath(path: String) {
unlikelyToBeNull(mPaths.value).remove(path) unlikelyToBeNull(_paths.value).remove(path)
mPaths.value = mPaths.value _paths.value = _paths.value
isModified = true isModified = true
} }
@ -75,7 +75,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
fun save(onDone: () -> Unit) { fun save(onDone: () -> Unit) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
excludedDatabase.writePaths(unlikelyToBeNull(mPaths.value)) excludedDatabase.writePaths(unlikelyToBeNull(_paths.value))
isModified = false isModified = false
onDone() onDone()
this@ExcludedViewModel.logD( this@ExcludedViewModel.logD(
@ -90,7 +90,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
isModified = false isModified = false
val dbPaths = excludedDatabase.readPaths() val dbPaths = excludedDatabase.readPaths()
withContext(Dispatchers.Main) { mPaths.value = dbPaths.toMutableList() } withContext(Dispatchers.Main) { _paths.value = dbPaths.toMutableList() }
this@ExcludedViewModel.logD( this@ExcludedViewModel.logD(
"Path load completed successfully in ${System.currentTimeMillis() - start}ms") "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.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.widget.Toolbar
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels 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.databinding.FragmentPlaybackPanelBinding
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.toDuration
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.clamp import org.oxycblt.auxio.util.clamp
import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.getAttrColorSafe import org.oxycblt.auxio.util.getAttrColorSafe
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.stateList import org.oxycblt.auxio.util.stateList
@ -55,7 +56,8 @@ import org.oxycblt.auxio.util.textSafe
class PlaybackPanelFragment : class PlaybackPanelFragment :
ViewBindingFragment<FragmentPlaybackPanelBinding>(), ViewBindingFragment<FragmentPlaybackPanelBinding>(),
Slider.OnChangeListener, Slider.OnChangeListener,
Slider.OnSliderTouchListener { Slider.OnSliderTouchListener,
Toolbar.OnMenuItemClickListener {
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
private val navModel: NavigationViewModel by activityViewModels() private val navModel: NavigationViewModel by activityViewModels()
@ -77,16 +79,7 @@ class PlaybackPanelFragment :
binding.playbackToolbar.apply { binding.playbackToolbar.apply {
setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.COLLAPSE) } setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.COLLAPSE) }
setOnMenuItemClickListener(this@PlaybackPanelFragment)
setOnMenuItemClickListener { item ->
if (item.itemId == R.id.action_queue) {
navModel.mainNavigateTo(MainNavigationAction.QUEUE)
true
} else {
false
}
}
queueItem = menu.findItem(R.id.action_queue) queueItem = menu.findItem(R.id.action_queue)
} }
@ -127,6 +120,8 @@ class PlaybackPanelFragment :
binding.playbackSkipNext.setOnClickListener { playbackModel.skipNext() } binding.playbackSkipNext.setOnClickListener { playbackModel.skipNext() }
binding.playbackShuffle.setOnClickListener { playbackModel.invertShuffled() } binding.playbackShuffle.setOnClickListener { playbackModel.invertShuffled() }
binding.playbackSeekBar.apply {}
// --- VIEWMODEL SETUP -- // --- VIEWMODEL SETUP --
playbackModel.song.observe(viewLifecycleOwner, ::updateSong) playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
@ -146,11 +141,22 @@ class PlaybackPanelFragment :
} }
override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) { override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) {
binding.playbackToolbar.setOnMenuItemClickListener(null)
binding.playbackSong.isSelected = false binding.playbackSong.isSelected = false
binding.playbackSeekBar.removeOnChangeListener(this) binding.playbackSeekBar.removeOnChangeListener(this)
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) { override fun onStartTrackingTouch(slider: Slider) {
requireBinding().playbackPosition.isActivated = true requireBinding().playbackPosition.isActivated = true
} }
@ -162,7 +168,7 @@ class PlaybackPanelFragment :
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) { override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
if (fromUser) { 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 // Normally if a song had a duration
val seconds = song.seconds val seconds = song.seconds
binding.playbackDuration.textSafe = seconds.toDuration(false) binding.playbackDuration.textSafe = seconds.formatDuration(false)
binding.playbackSeekBar.apply { binding.playbackSeekBar.apply {
isEnabled = seconds > 0L isEnabled = seconds > 0L
valueTo = max(seconds, 1L).toFloat() valueTo = max(seconds, 1L).toFloat()
@ -197,7 +203,7 @@ class PlaybackPanelFragment :
val binding = requireBinding() val binding = requireBinding()
if (!binding.playbackPosition.isActivated) { if (!binding.playbackPosition.isActivated) {
binding.playbackSeekBar.value = position.toFloat() 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 settingsManager = SettingsManager.getInstance()
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
// Playback private var intentUri: Uri? = null
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 val _song = MutableLiveData<Song?>()
/** The current song. */ /** The current song. */
val song: LiveData<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] */ /** The current model that is being played from, such as an [Album] or [Artist] */
val parent: LiveData<MusicParent?> val parent: LiveData<MusicParent?>
get() = mParent get() = _parent
private val _isPlaying = MutableLiveData(false)
val isPlaying: LiveData<Boolean> val isPlaying: LiveData<Boolean>
get() = mIsPlaying get() = _isPlaying
private val _positionSecs = MutableLiveData(0L)
/** The current playback position, in seconds */ /** The current playback position, in seconds */
val positionSecs: LiveData<Long> val positionSecs: LiveData<Long>
get() = mPositionSecs get() = _positionSecs
private val _repeatMode = MutableLiveData(RepeatMode.NONE)
/** The current repeat mode, see [RepeatMode] for more information */ /** The current repeat mode, see [RepeatMode] for more information */
val repeatMode: LiveData<RepeatMode> val repeatMode: LiveData<RepeatMode>
get() = mRepeatMode get() = _repeatMode
private val _isShuffled = MutableLiveData(false)
val isShuffled: LiveData<Boolean> val isShuffled: LiveData<Boolean>
get() = mIsShuffled get() = _isShuffled
private val _nextUp = MutableLiveData(listOf<Song>())
/** The queue, without the previous items. */ /** The queue, without the previous items. */
val nextUp: LiveData<List<Song>> val nextUp: LiveData<List<Song>>
get() = mNextUp get() = _nextUp
init { init {
playbackManager.addCallback(this) playbackManager.addCallback(this)
@ -167,7 +159,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
} else { } else {
logD("Cant play this URI right now, waiting") 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) { fun removeQueueDataItem(adapterIndex: Int, apply: () -> Unit) {
val index = val index =
adapterIndex + (playbackManager.queue.size - unlikelyToBeNull(mNextUp.value).size) adapterIndex + (playbackManager.queue.size - unlikelyToBeNull(_nextUp.value).size)
if (index in playbackManager.queue.indices) { if (index in playbackManager.queue.indices) {
apply() apply()
playbackManager.removeQueueItem(index) 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. * is called just before the change is committed so that the adapter can be updated.
*/ */
fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int, apply: () -> Unit): Boolean { 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 from = adapterFrom + delta
val to = adapterTo + delta val to = adapterTo + delta
if (from in playbackManager.queue.indices && to in playbackManager.queue.indices) { 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. * - Restore the last playback state if there is no active file intent.
*/ */
fun setupPlayback(context: Context) { fun setupPlayback(context: Context) {
val intentUri = mIntentUri val intentUri = intentUri
if (intentUri != null) { if (intentUri != null) {
playWithUriInternal(intentUri, context) playWithUriInternal(intentUri, context)
// Remove the uri after finishing the calls so that this does not fire again. // Remove the uri after finishing the calls so that this does not fire again.
mIntentUri = null this.intentUri = null
} else if (!playbackManager.isInitialized) { } else if (!playbackManager.isInitialized) {
// Otherwise just restore // Otherwise just restore
viewModelScope.launch { playbackManager.restoreState(context) } viewModelScope.launch { playbackManager.restoreState(context) }
@ -319,33 +311,33 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
} }
override fun onIndexMoved(index: Int) { override fun onIndexMoved(index: Int) {
mSong.value = playbackManager.song _song.value = playbackManager.song
mNextUp.value = playbackManager.queue.slice(index.inc() until playbackManager.queue.size) _nextUp.value = playbackManager.queue.slice(index + 1 until playbackManager.queue.size)
} }
override fun onQueueChanged(index: Int, queue: List<Song>) { 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?) { override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
mParent.value = playbackManager.parent _parent.value = playbackManager.parent
mSong.value = playbackManager.song _song.value = playbackManager.song
mNextUp.value = queue.slice(index.inc() until queue.size) _nextUp.value = queue.slice(index + 1 until queue.size)
} }
override fun onPositionChanged(positionMs: Long) { override fun onPositionChanged(positionMs: Long) {
mPositionSecs.value = positionMs / 1000 _positionSecs.value = positionMs / 1000
} }
override fun onPlayingChanged(isPlaying: Boolean) { override fun onPlayingChanged(isPlaying: Boolean) {
mIsPlaying.value = isPlaying _isPlaying.value = isPlaying
} }
override fun onShuffledChanged(isShuffled: Boolean) { override fun onShuffledChanged(isShuffled: Boolean) {
mIsShuffled.value = isShuffled _isShuffled.value = isShuffled
} }
override fun onRepeatChanged(repeatMode: RepeatMode) { 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<*>, private val adapter: RecyclerView.Adapter<*>,
diffCallback: DiffUtil.ItemCallback<T> diffCallback: DiffUtil.ItemCallback<T>
) : BackingData<T>() { ) : BackingData<T>() {
private var mCurrentList = mutableListOf<T>() private var _currentList = mutableListOf<T>()
val currentList: List<T> val currentList: List<T>
get() = mCurrentList get() = _currentList
private val differ = AsyncListDiffer(adapter, diffCallback) private val differ = AsyncListDiffer(adapter, diffCallback)
override fun getItem(position: Int): T = mCurrentList[position] override fun getItem(position: Int): T = _currentList[position]
override fun getItemCount(): Int = mCurrentList.size override fun getItemCount(): Int = _currentList.size
fun submitList(newData: List<T>, onDone: () -> Unit = {}) { fun submitList(newData: List<T>, onDone: () -> Unit = {}) {
if (newData != mCurrentList) { if (newData != _currentList) {
mCurrentList = newData.toMutableList() _currentList = newData.toMutableList()
differ.submitList(newData, onDone) differ.submitList(newData, onDone)
} }
} }
fun moveItems(from: Int, to: Int) { fun moveItems(from: Int, to: Int) {
mCurrentList.add(to, mCurrentList.removeAt(from)) _currentList.add(to, _currentList.removeAt(from))
differ.rewriteListUnsafe(mCurrentList) differ.rewriteListUnsafe(_currentList)
adapter.notifyItemMoved(from, to) adapter.notifyItemMoved(from, to)
} }
fun removeItem(at: Int) { fun removeItem(at: Int) {
mCurrentList.removeAt(at) _currentList.removeAt(at)
differ.rewriteListUnsafe(mCurrentList) differ.rewriteListUnsafe(_currentList)
adapter.notifyItemRemoved(at) adapter.notifyItemRemoved(at)
} }
@ -152,7 +152,7 @@ class HybridBackingData<T>(
* can do to marry the adapter primitives with DiffUtil. * can do to marry the adapter primitives with DiffUtil.
*/ */
private fun <T> AsyncListDiffer<T>.rewriteListUnsafe(newList: List<T>) { 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()) differListField.set(this, newList.toMutableList())
differImmutableListField.set(this, newList) differImmutableListField.set(this, newList)
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -24,36 +24,39 @@ import org.oxycblt.auxio.music.Music
/** A ViewModel that handles complicated navigation situations. */ /** A ViewModel that handles complicated navigation situations. */
class NavigationViewModel : ViewModel() { class NavigationViewModel : ViewModel() {
private val mMainNavigationAction = MutableLiveData<MainNavigationAction?>() private val _mainNavigationAction = MutableLiveData<MainNavigationAction?>()
/** Flag for main fragment navigation. Intended for MainFragment use only. */ /** Flag for main fragment navigation. Intended for MainFragment use only. */
val mainNavigationAction: LiveData<MainNavigationAction?> val mainNavigationAction: LiveData<MainNavigationAction?>
get() = mMainNavigationAction get() = _mainNavigationAction
private val mExploreNavigationItem = MutableLiveData<Music?>() private val _exploreNavigationItem = MutableLiveData<Music?>()
/** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */ /**
* Flag for navigation within the explore fragments. Observe this to coordinate navigation to an
* item's UI.
*/
val exploreNavigationItem: LiveData<Music?> val exploreNavigationItem: LiveData<Music?>
get() = mExploreNavigationItem get() = _exploreNavigationItem
/** Notify MainFragment to navigate to the location outlined in [MainNavigationAction]. */ /** Notify MainFragment to navigate to the location outlined in [MainNavigationAction]. */
fun mainNavigateTo(action: MainNavigationAction) { fun mainNavigateTo(action: MainNavigationAction) {
if (mMainNavigationAction.value != null) return if (_mainNavigationAction.value != null) return
mMainNavigationAction.value = action _mainNavigationAction.value = action
} }
/** Mark that the main navigation process is done. */ /** Mark that the main navigation process is done. */
fun finishMainNavigation() { fun finishMainNavigation() {
mMainNavigationAction.value = null _mainNavigationAction.value = null
} }
/** Navigate to an item's detail menu, whether a song/album/artist */ /** Navigate to an item's detail menu, whether a song/album/artist */
fun exploreNavigateTo(item: Music) { fun exploreNavigateTo(item: Music) {
if (mExploreNavigationItem.value != null) return if (_exploreNavigationItem.value != null) return
mExploreNavigationItem.value = item _exploreNavigationItem.value = item
} }
/** Mark that the item navigation process is done. */ /** Mark that the item navigation process is done. */
fun finishExploreNavigation() { 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. * [AsyncBackingData] is not preferable due to bugs involving diffing.
*/ */
class PrimitiveBackingData<T>(private val adapter: RecyclerView.Adapter<*>) : BackingData<T>() { 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. */ /** The current list backing this adapter. */
val currentList: List<T> val currentList: List<T>
get() = mCurrentList get() = _currentList
override fun getItem(position: Int): T = mCurrentList[position] override fun getItem(position: Int): T = _currentList[position]
override fun getItemCount(): Int = mCurrentList.size override fun getItemCount(): Int = _currentList.size
/** /**
* Update the list with a [newList]. This calls [RecyclerView.Adapter.notifyDataSetChanged] * 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") @Suppress("NotifyDatasetChanged")
fun submitList(newList: List<T>) { fun submitList(newList: List<T>) {
mCurrentList = newList.toMutableList() _currentList = newList.toMutableList()
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
} }

View file

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

View file

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

View file

@ -18,6 +18,7 @@
package org.oxycblt.auxio.util package org.oxycblt.auxio.util
import android.os.Looper import android.os.Looper
import android.text.format.DateUtils
import androidx.core.math.MathUtils import androidx.core.math.MathUtils
import org.oxycblt.auxio.BuildConfig 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] */ /** Shortcut to clamp an integer between [min] and [max] */
fun Long.clamp(min: Long, max: Long): Long = MathUtils.clamp(this, min, 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"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- Note: Not actually transparent, making it transparent would actually make it translucent -->
<color name="chrome_transparent">#01151515</color> <color name="chrome_transparent">#01151515</color>
<color name="red_primary">#FFB4A8</color> <color name="red_primary">#FFB4A8</color>

View file

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

View file

@ -9,7 +9,7 @@ buildscript {
} }
dependencies { 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 "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version"
classpath "com.diffplug.spotless:spotless-plugin-gradle:6.3.0" classpath "com.diffplug.spotless:spotless-plugin-gradle:6.3.0"