Update documentation
Completely update the documentation throughout the app.
This commit is contained in:
parent
b9720286c3
commit
e9abee9f64
46 changed files with 481 additions and 415 deletions
|
@ -60,11 +60,11 @@ class MainActivity : AppCompatActivity() {
|
||||||
// to PlaybackViewModel to be used later.
|
// to PlaybackViewModel to be used later.
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
val action = intent.action
|
val action = intent.action
|
||||||
val isConsumed = intent.getBooleanExtra(KEY_INTENT_CONSUMED, false)
|
val isConsumed = intent.getBooleanExtra(KEY_INTENT_USED, false)
|
||||||
|
|
||||||
if (action == Intent.ACTION_VIEW && !isConsumed) {
|
if (action == Intent.ACTION_VIEW && !isConsumed) {
|
||||||
// Mark the intent as used so this does not fire again
|
// Mark the intent as used so this does not fire again
|
||||||
intent.putExtra(KEY_INTENT_CONSUMED, true)
|
intent.putExtra(KEY_INTENT_USED, true)
|
||||||
|
|
||||||
intent.data?.let { fileUri ->
|
intent.data?.let { fileUri ->
|
||||||
playbackModel.playWithUri(fileUri, this)
|
playbackModel.playWithUri(fileUri, this)
|
||||||
|
@ -100,6 +100,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val KEY_INTENT_CONSUMED = "KEY_FILE_INTENT_USED"
|
private const val KEY_INTENT_USED = "KEY_FILE_INTENT_USED"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import java.io.InputStream
|
||||||
/**
|
/**
|
||||||
* Fetcher that returns the album art for a given [Album]. Handles settings on whether to use
|
* Fetcher that returns the album art for a given [Album]. Handles settings on whether to use
|
||||||
* quality covers or not.
|
* quality covers or not.
|
||||||
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class AlbumArtFetcher(private val context: Context) : Fetcher<Album> {
|
class AlbumArtFetcher(private val context: Context) : Fetcher<Album> {
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
|
@ -19,7 +19,7 @@ import org.oxycblt.auxio.settings.SettingsManager
|
||||||
// --- BINDING ADAPTERS ---
|
// --- BINDING ADAPTERS ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind the album art for a [Song].
|
* Bind the album art for a [song].
|
||||||
*/
|
*/
|
||||||
@BindingAdapter("albumArt")
|
@BindingAdapter("albumArt")
|
||||||
fun ImageView.bindAlbumArt(song: Song) {
|
fun ImageView.bindAlbumArt(song: Song) {
|
||||||
|
@ -27,7 +27,7 @@ fun ImageView.bindAlbumArt(song: Song) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind the album art for an [Album].
|
* Bind the album art for an [album].
|
||||||
*/
|
*/
|
||||||
@BindingAdapter("albumArt")
|
@BindingAdapter("albumArt")
|
||||||
fun ImageView.bindAlbumArt(album: Album) {
|
fun ImageView.bindAlbumArt(album: Album) {
|
||||||
|
@ -35,7 +35,7 @@ fun ImageView.bindAlbumArt(album: Album) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind the image for an [Artist]
|
* Bind the image for an [artist]
|
||||||
*/
|
*/
|
||||||
@BindingAdapter("artistImage")
|
@BindingAdapter("artistImage")
|
||||||
fun ImageView.bindArtistImage(artist: Artist) {
|
fun ImageView.bindArtistImage(artist: Artist) {
|
||||||
|
@ -43,7 +43,7 @@ fun ImageView.bindArtistImage(artist: Artist) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind the image for a [Genre]
|
* Bind the image for a [genre]
|
||||||
*/
|
*/
|
||||||
@BindingAdapter("genreImage")
|
@BindingAdapter("genreImage")
|
||||||
fun ImageView.bindGenreImage(genre: Genre) {
|
fun ImageView.bindGenreImage(genre: Genre) {
|
||||||
|
|
|
@ -2,7 +2,6 @@ package org.oxycblt.auxio.database
|
||||||
|
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import android.database.sqlite.SQLiteOpenHelper
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
@ -35,9 +34,7 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
// --- DATABASE CONSTRUCTION FUNCTIONS ---
|
// --- DATABASE CONSTRUCTION FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a table
|
* Create a table for this database.
|
||||||
* @param database DB to create the tables on
|
|
||||||
* @param tableName The name of the table to create.
|
|
||||||
*/
|
*/
|
||||||
private fun createTable(database: SQLiteDatabase, tableName: String) {
|
private fun createTable(database: SQLiteDatabase, tableName: String) {
|
||||||
val command = StringBuilder()
|
val command = StringBuilder()
|
||||||
|
@ -106,15 +103,17 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
|
|
||||||
val stateData = ContentValues(9)
|
val stateData = ContentValues(9)
|
||||||
|
|
||||||
stateData.put(PlaybackState.COLUMN_ID, state.id)
|
stateData.apply {
|
||||||
stateData.put(PlaybackState.COLUMN_SONG_NAME, state.songName)
|
put(PlaybackState.COLUMN_ID, state.id)
|
||||||
stateData.put(PlaybackState.COLUMN_POSITION, state.position)
|
put(PlaybackState.COLUMN_SONG_NAME, state.songName)
|
||||||
stateData.put(PlaybackState.COLUMN_PARENT_NAME, state.parentName)
|
put(PlaybackState.COLUMN_POSITION, state.position)
|
||||||
stateData.put(PlaybackState.COLUMN_INDEX, state.index)
|
put(PlaybackState.COLUMN_PARENT_NAME, state.parentName)
|
||||||
stateData.put(PlaybackState.COLUMN_MODE, state.mode)
|
put(PlaybackState.COLUMN_INDEX, state.index)
|
||||||
stateData.put(PlaybackState.COLUMN_IS_SHUFFLING, state.isShuffling)
|
put(PlaybackState.COLUMN_MODE, state.mode)
|
||||||
stateData.put(PlaybackState.COLUMN_LOOP_MODE, state.loopMode)
|
put(PlaybackState.COLUMN_IS_SHUFFLING, state.isShuffling)
|
||||||
stateData.put(PlaybackState.COLUMN_IN_USER_QUEUE, state.inUserQueue)
|
put(PlaybackState.COLUMN_LOOP_MODE, state.loopMode)
|
||||||
|
put(PlaybackState.COLUMN_IN_USER_QUEUE, state.inUserQueue)
|
||||||
|
}
|
||||||
|
|
||||||
database.insert(TABLE_NAME_STATE, null, stateData)
|
database.insert(TABLE_NAME_STATE, null, stateData)
|
||||||
database.setTransactionSuccessful()
|
database.setTransactionSuccessful()
|
||||||
|
@ -135,10 +134,9 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
val database = writableDatabase
|
val database = writableDatabase
|
||||||
|
|
||||||
var state: PlaybackState? = null
|
var state: PlaybackState? = null
|
||||||
val stateCursor: Cursor
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
stateCursor = database.query(
|
val stateCursor = database.query(
|
||||||
TABLE_NAME_STATE,
|
TABLE_NAME_STATE,
|
||||||
null, null, null,
|
null, null, null,
|
||||||
null, null, null
|
null, null, null
|
||||||
|
@ -179,10 +177,9 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write a list of [QueueItem]s to the database, clearing the previous queue present.
|
* Write a list of [queueItems] to the database, clearing the previous queue present.
|
||||||
* @param queue The list of [QueueItem]s to be written.
|
|
||||||
*/
|
*/
|
||||||
fun writeQueue(queue: List<QueueItem>) {
|
fun writeQueue(queueItems: List<QueueItem>) {
|
||||||
assertBackgroundThread()
|
assertBackgroundThread()
|
||||||
|
|
||||||
val database = readableDatabase
|
val database = readableDatabase
|
||||||
|
@ -202,21 +199,23 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
var position = 0
|
var position = 0
|
||||||
|
|
||||||
// Try to write out the entirety of the queue, any failed inserts will be skipped.
|
// Try to write out the entirety of the queue, any failed inserts will be skipped.
|
||||||
while (position < queue.size) {
|
while (position < queueItems.size) {
|
||||||
database.beginTransaction()
|
database.beginTransaction()
|
||||||
var i = position
|
var i = position
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (i < queue.size) {
|
while (i < queueItems.size) {
|
||||||
val item = queue[i]
|
val item = queueItems[i]
|
||||||
val itemData = ContentValues(4)
|
val itemData = ContentValues(4)
|
||||||
|
|
||||||
i++
|
i++
|
||||||
|
|
||||||
itemData.put(QueueItem.COLUMN_ID, item.id)
|
itemData.apply {
|
||||||
itemData.put(QueueItem.COLUMN_SONG_NAME, item.songName)
|
put(QueueItem.COLUMN_ID, item.id)
|
||||||
itemData.put(QueueItem.COLUMN_ALBUM_NAME, item.albumName)
|
put(QueueItem.COLUMN_SONG_NAME, item.songName)
|
||||||
itemData.put(QueueItem.COLUMN_IS_USER_QUEUE, item.isUserQueue)
|
put(QueueItem.COLUMN_ALBUM_NAME, item.albumName)
|
||||||
|
put(QueueItem.COLUMN_IS_USER_QUEUE, item.isUserQueue)
|
||||||
|
}
|
||||||
|
|
||||||
database.insert(TABLE_NAME_QUEUE, null, itemData)
|
database.insert(TABLE_NAME_QUEUE, null, itemData)
|
||||||
}
|
}
|
||||||
|
@ -242,12 +241,10 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
assertBackgroundThread()
|
assertBackgroundThread()
|
||||||
|
|
||||||
val database = readableDatabase
|
val database = readableDatabase
|
||||||
|
|
||||||
val queueItems = mutableListOf<QueueItem>()
|
val queueItems = mutableListOf<QueueItem>()
|
||||||
val queueCursor: Cursor
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
queueCursor = database.query(
|
val queueCursor = database.query(
|
||||||
TABLE_NAME_QUEUE, null, null,
|
TABLE_NAME_QUEUE, null, null,
|
||||||
null, null, null, null
|
null, null, null, null
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,6 +17,7 @@ import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
import org.oxycblt.auxio.playback.state.PlaybackMode
|
||||||
import org.oxycblt.auxio.recycler.CenterSmoothScroller
|
import org.oxycblt.auxio.recycler.CenterSmoothScroller
|
||||||
import org.oxycblt.auxio.ui.ActionMenu
|
import org.oxycblt.auxio.ui.ActionMenu
|
||||||
|
import org.oxycblt.auxio.ui.canScroll
|
||||||
import org.oxycblt.auxio.ui.createToast
|
import org.oxycblt.auxio.ui.createToast
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
|
|
||||||
|
@ -83,7 +84,6 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
detailModel.navToItem.observe(viewLifecycleOwner) {
|
detailModel.navToItem.observe(viewLifecycleOwner) {
|
||||||
if (it != null) {
|
|
||||||
when (it) {
|
when (it) {
|
||||||
// Songs should be scrolled to if the album matches, or a new detail
|
// Songs should be scrolled to if the album matches, or a new detail
|
||||||
// fragment should be launched otherwise.
|
// fragment should be launched otherwise.
|
||||||
|
@ -122,7 +122,6 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// --- PLAYBACKVIEWMODEL SETUP ---
|
// --- PLAYBACKVIEWMODEL SETUP ---
|
||||||
|
|
||||||
|
@ -148,6 +147,9 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll to an song using its [id].
|
||||||
|
*/
|
||||||
private fun scrollToItem(id: Long) {
|
private fun scrollToItem(id: Long) {
|
||||||
// Calculate where the item for the currently played song is
|
// Calculate where the item for the currently played song is
|
||||||
val pos = detailModel.albumSortMode.value!!.getSortedSongList(
|
val pos = detailModel.albumSortMode.value!!.getSortedSongList(
|
||||||
|
@ -164,9 +166,7 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
// If the recyclerview can scroll, its certain that it will have to scroll to
|
// If the recyclerview can scroll, its certain that it will have to scroll to
|
||||||
// correctly center the playing item, so make sure that the Toolbar is lifted in
|
// correctly center the playing item, so make sure that the Toolbar is lifted in
|
||||||
// that case.
|
// that case.
|
||||||
if (binding.detailRecycler.computeVerticalScrollRange() > binding.detailRecycler.height) {
|
binding.detailAppbar.isLifted = binding.detailRecycler.canScroll()
|
||||||
binding.detailAppbar.isLifted = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,8 @@ abstract class DetailFragment : Fragment() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut method for doing setup of the detail toolbar.
|
* Shortcut method for doing setup of the detail toolbar.
|
||||||
|
* @param menu Menu resource to use
|
||||||
|
* @param onMenuClick (Optional) a click listener for that menu
|
||||||
*/
|
*/
|
||||||
protected fun setupToolbar(
|
protected fun setupToolbar(
|
||||||
@MenuRes menu: Int = -1,
|
@MenuRes menu: Int = -1,
|
||||||
|
|
|
@ -12,40 +12,43 @@ import org.oxycblt.auxio.recycler.SortMode
|
||||||
/**
|
/**
|
||||||
* ViewModel that stores data for the [DetailFragment]s, such as what they're showing & what
|
* ViewModel that stores data for the [DetailFragment]s, such as what they're showing & what
|
||||||
* [SortMode] they are currently on.
|
* [SortMode] they are currently on.
|
||||||
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class DetailViewModel : ViewModel() {
|
class DetailViewModel : ViewModel() {
|
||||||
private var mIsNavigating = false
|
private val mCurrentGenre = MutableLiveData<Genre?>()
|
||||||
val isNavigating: Boolean get() = mIsNavigating
|
private val mCurrentArtist = MutableLiveData<Artist?>()
|
||||||
|
private val mCurrentAlbum = MutableLiveData<Album?>()
|
||||||
|
|
||||||
private val mGenreSortMode = MutableLiveData(SortMode.ALPHA_DOWN)
|
private val mGenreSortMode = MutableLiveData(SortMode.ALPHA_DOWN)
|
||||||
val genreSortMode: LiveData<SortMode> get() = mGenreSortMode
|
|
||||||
|
|
||||||
private val mArtistSortMode = MutableLiveData(SortMode.NUMERIC_DOWN)
|
private val mArtistSortMode = MutableLiveData(SortMode.NUMERIC_DOWN)
|
||||||
val artistSortMode: LiveData<SortMode> get() = mArtistSortMode
|
|
||||||
|
|
||||||
private val mAlbumSortMode = MutableLiveData(SortMode.NUMERIC_DOWN)
|
private val mAlbumSortMode = MutableLiveData(SortMode.NUMERIC_DOWN)
|
||||||
val albumSortMode: LiveData<SortMode> get() = mAlbumSortMode
|
|
||||||
|
|
||||||
// Current music models being shown
|
private val mNavToItem = MutableLiveData<BaseModel?>()
|
||||||
private val mCurrentGenre = MutableLiveData<Genre?>()
|
private var mIsNavigating = false
|
||||||
|
|
||||||
val currentGenre: LiveData<Genre?> get() = mCurrentGenre
|
val currentGenre: LiveData<Genre?> get() = mCurrentGenre
|
||||||
|
|
||||||
private val mCurrentArtist = MutableLiveData<Artist?>()
|
|
||||||
val currentArtist: LiveData<Artist?> get() = mCurrentArtist
|
val currentArtist: LiveData<Artist?> get() = mCurrentArtist
|
||||||
|
|
||||||
private val mCurrentAlbum = MutableLiveData<Album?>()
|
|
||||||
val currentAlbum: LiveData<Album?> get() = mCurrentAlbum
|
val currentAlbum: LiveData<Album?> get() = mCurrentAlbum
|
||||||
|
|
||||||
// Primary navigation flag.
|
val genreSortMode: LiveData<SortMode> get() = mGenreSortMode
|
||||||
private val mNavToItem = MutableLiveData<BaseModel?>()
|
val albumSortMode: LiveData<SortMode> get() = mAlbumSortMode
|
||||||
|
val artistSortMode: LiveData<SortMode> get() = mArtistSortMode
|
||||||
|
|
||||||
|
val isNavigating: Boolean get() = mIsNavigating
|
||||||
|
|
||||||
|
/** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */
|
||||||
val navToItem: LiveData<BaseModel?> get() = mNavToItem
|
val navToItem: LiveData<BaseModel?> get() = mNavToItem
|
||||||
|
|
||||||
/**
|
fun updateGenre(genre: Genre) {
|
||||||
* Update the current navigation status
|
mCurrentGenre.value = genre
|
||||||
* @param value Whether the current [DetailFragment] is navigating or not.
|
}
|
||||||
*/
|
|
||||||
fun updateNavigationStatus(value: Boolean) {
|
fun updateArtist(artist: Artist) {
|
||||||
mIsNavigating = value
|
mCurrentArtist.value = artist
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateAlbum(album: Album) {
|
||||||
|
mCurrentAlbum.value = album
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -75,7 +78,7 @@ class DetailViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Increment the sort mode of the album songs
|
* Increment the sort mode of the album song
|
||||||
*/
|
*/
|
||||||
fun incrementAlbumSortMode() {
|
fun incrementAlbumSortMode() {
|
||||||
mAlbumSortMode.value = when (mAlbumSortMode.value) {
|
mAlbumSortMode.value = when (mAlbumSortMode.value) {
|
||||||
|
@ -86,25 +89,24 @@ class DetailViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateGenre(genre: Genre) {
|
/**
|
||||||
mCurrentGenre.value = genre
|
* Navigate to an item, whether a song/album/artist
|
||||||
}
|
*/
|
||||||
|
|
||||||
fun updateArtist(artist: Artist) {
|
|
||||||
mCurrentArtist.value = artist
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateAlbum(album: Album) {
|
|
||||||
mCurrentAlbum.value = album
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Navigate to an item, whether a song/album/artist */
|
|
||||||
fun navToItem(item: BaseModel) {
|
fun navToItem(item: BaseModel) {
|
||||||
mNavToItem.value = item
|
mNavToItem.value = item
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mark that the navigation process is done. */
|
/**
|
||||||
|
* Mark that the navigation process is done.
|
||||||
|
*/
|
||||||
fun doneWithNavToItem() {
|
fun doneWithNavToItem() {
|
||||||
mNavToItem.value = null
|
mNavToItem.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the current navigation status to [isNavigating]
|
||||||
|
*/
|
||||||
|
fun updateNavigationStatus(isNavigating: Boolean) {
|
||||||
|
mIsNavigating = isNavigating
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,6 @@ class GenreDetailFragment : DetailFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
detailModel.navToItem.observe(viewLifecycleOwner) {
|
detailModel.navToItem.observe(viewLifecycleOwner) {
|
||||||
if (it != null) {
|
|
||||||
when (it) {
|
when (it) {
|
||||||
// All items will launch new detail fragments.
|
// All items will launch new detail fragments.
|
||||||
is Artist -> findNavController().navigate(
|
is Artist -> findNavController().navigate(
|
||||||
|
@ -88,7 +87,6 @@ class GenreDetailFragment : DetailFragment() {
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// --- PLAYBACKVIEWMODEL SETUP ---
|
// --- PLAYBACKVIEWMODEL SETUP ---
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.oxycblt.auxio.ui.setTextColorResource
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An adapter for displaying the details and [Song]s of an [Album]
|
* An adapter for displaying the details and [Song]s of an [Album]
|
||||||
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class AlbumDetailAdapter(
|
class AlbumDetailAdapter(
|
||||||
private val detailModel: DetailViewModel,
|
private val detailModel: DetailViewModel,
|
||||||
|
@ -75,8 +76,8 @@ class AlbumDetailAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the current song that this adapter should be watching for to highlight.
|
* Update the [song] that this adapter should highlight
|
||||||
* @param song The [Song] to highlight if found, null to clear any highlighted ViewHolders
|
* @param recycler The recyclerview the highlighting should act on.
|
||||||
*/
|
*/
|
||||||
fun highlightSong(song: Song?, recycler: RecyclerView) {
|
fun highlightSong(song: Song?, recycler: RecyclerView) {
|
||||||
// Clear out the last ViewHolder as a song update usually signifies that this current
|
// Clear out the last ViewHolder as a song update usually signifies that this current
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.oxycblt.auxio.ui.setTextColorResource
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An adapter for displaying the [Album]s of an artist.
|
* An adapter for displaying the [Album]s of an artist.
|
||||||
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class ArtistDetailAdapter(
|
class ArtistDetailAdapter(
|
||||||
private val detailModel: DetailViewModel,
|
private val detailModel: DetailViewModel,
|
||||||
|
@ -76,8 +77,8 @@ class ArtistDetailAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the current album that this adapter should be watching for to highlight.
|
* Update the current [album] that this adapter should highlight
|
||||||
* @param album The [Album] to highlight if found, null to clear any highlighted ViewHolders
|
* @param recycler The recyclerview the highlighting should act on.
|
||||||
*/
|
*/
|
||||||
fun setCurrentAlbum(album: Album?, recycler: RecyclerView) {
|
fun setCurrentAlbum(album: Album?, recycler: RecyclerView) {
|
||||||
// Clear out the last ViewHolder as a song update usually signifies that this current
|
// Clear out the last ViewHolder as a song update usually signifies that this current
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.oxycblt.auxio.ui.setTextColorResource
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An adapter for displaying the [Song]s of a genre.
|
* An adapter for displaying the [Song]s of a genre.
|
||||||
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class GenreDetailAdapter(
|
class GenreDetailAdapter(
|
||||||
private val detailModel: DetailViewModel,
|
private val detailModel: DetailViewModel,
|
||||||
|
@ -76,8 +77,8 @@ class GenreDetailAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the current song that this adapter should be watching for to highlight.
|
* Update the [song] that this adapter should highlight
|
||||||
* @param song The [Song] to highlight if found, null to clear any highlighted ViewHolders
|
* @param recycler The recyclerview the highlighting should act on.
|
||||||
*/
|
*/
|
||||||
fun highlightSong(song: Song?, recycler: RecyclerView) {
|
fun highlightSong(song: Song?, recycler: RecyclerView) {
|
||||||
// Clear out the last ViewHolder as a song update usually signifies that this current
|
// Clear out the last ViewHolder as a song update usually signifies that this current
|
||||||
|
|
|
@ -59,8 +59,7 @@ class LibraryAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the data directly. [notifyDataSetChanged] will be called
|
* Update the data with [newData]. [notifyDataSetChanged] will be called.
|
||||||
* @param newData The new data to be used
|
|
||||||
*/
|
*/
|
||||||
fun updateData(newData: List<Parent>) {
|
fun updateData(newData: List<Parent>) {
|
||||||
data = newData
|
data = newData
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.oxycblt.auxio.ui.newMenu
|
||||||
/**
|
/**
|
||||||
* A [Fragment] that shows a custom list of [Genre], [Artist], or [Album] data. Also allows for
|
* A [Fragment] that shows a custom list of [Genre], [Artist], or [Album] data. Also allows for
|
||||||
* search functionality.
|
* search functionality.
|
||||||
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class LibraryFragment : Fragment() {
|
class LibraryFragment : Fragment() {
|
||||||
private val libraryModel: LibraryViewModel by activityViewModels()
|
private val libraryModel: LibraryViewModel by activityViewModels()
|
||||||
|
@ -36,7 +37,7 @@ class LibraryFragment : Fragment() {
|
||||||
): View {
|
): View {
|
||||||
val binding = FragmentLibraryBinding.inflate(inflater)
|
val binding = FragmentLibraryBinding.inflate(inflater)
|
||||||
|
|
||||||
val libraryAdapter = LibraryAdapter(::onItemSelection) { view, data -> newMenu(view, data) }
|
val libraryAdapter = LibraryAdapter(::navToDetail) { view, data -> newMenu(view, data) }
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
|
|
||||||
|
@ -78,9 +79,9 @@ class LibraryFragment : Fragment() {
|
||||||
libraryModel.updateNavigationStatus(false)
|
libraryModel.updateNavigationStatus(false)
|
||||||
|
|
||||||
if (it is Parent) {
|
if (it is Parent) {
|
||||||
onItemSelection(it)
|
navToDetail(it)
|
||||||
} else if (it is Song) {
|
} else if (it is Song) {
|
||||||
onItemSelection(it.album)
|
navToDetail(it.album)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,10 +104,9 @@ class LibraryFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to a parent UI
|
* Navigate to the detail UI for a [parent].
|
||||||
* @param parent The parent that should be navigated with
|
|
||||||
*/
|
*/
|
||||||
private fun onItemSelection(parent: Parent) {
|
private fun navToDetail(parent: Parent) {
|
||||||
requireView().rootView.clearFocus()
|
requireView().rootView.clearFocus()
|
||||||
|
|
||||||
if (!libraryModel.isNavigating) {
|
if (!libraryModel.isNavigating) {
|
||||||
|
|
|
@ -20,14 +20,14 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
private val mLibraryData = MutableLiveData(listOf<Parent>())
|
private val mLibraryData = MutableLiveData(listOf<Parent>())
|
||||||
val libraryData: LiveData<List<Parent>> get() = mLibraryData
|
val libraryData: LiveData<List<Parent>> get() = mLibraryData
|
||||||
|
|
||||||
private var mIsNavigating = false
|
|
||||||
val isNavigating: Boolean get() = mIsNavigating
|
|
||||||
|
|
||||||
private var mSortMode = SortMode.ALPHA_DOWN
|
private var mSortMode = SortMode.ALPHA_DOWN
|
||||||
val sortMode: SortMode get() = mSortMode
|
val sortMode: SortMode get() = mSortMode
|
||||||
|
|
||||||
private var mDisplayMode = DisplayMode.SHOW_ARTISTS
|
private var mDisplayMode = DisplayMode.SHOW_ARTISTS
|
||||||
|
|
||||||
|
private var mIsNavigating = false
|
||||||
|
val isNavigating: Boolean get() = mIsNavigating
|
||||||
|
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
|
|
||||||
|
@ -42,8 +42,7 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the current [SortMode] with a menu id.
|
* Update the current [SortMode] using an menu [itemId].
|
||||||
* @param itemId The id of the menu item selected.
|
|
||||||
*/
|
*/
|
||||||
fun updateSortMode(@IdRes itemId: Int) {
|
fun updateSortMode(@IdRes itemId: Int) {
|
||||||
val mode = when (itemId) {
|
val mode = when (itemId) {
|
||||||
|
@ -64,28 +63,11 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the current navigation status
|
* Update the current navigation status
|
||||||
* @param value Whether LibraryFragment is navigating or not
|
|
||||||
*/
|
*/
|
||||||
fun updateNavigationStatus(value: Boolean) {
|
fun updateNavigationStatus(isNavigating: Boolean) {
|
||||||
mIsNavigating = value
|
mIsNavigating = isNavigating
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- OVERRIDES ---
|
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
super.onCleared()
|
|
||||||
|
|
||||||
settingsManager.removeCallback(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLibDisplayModeUpdate(displayMode: DisplayMode) {
|
|
||||||
mDisplayMode = displayMode
|
|
||||||
|
|
||||||
updateLibraryData()
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- UTILS ---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut function for updating the library data with the current [SortMode]/[DisplayMode]
|
* Shortcut function for updating the library data with the current [SortMode]/[DisplayMode]
|
||||||
*/
|
*/
|
||||||
|
@ -100,4 +82,18 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
else -> error("DisplayMode $mDisplayMode is unsupported.")
|
else -> error("DisplayMode $mDisplayMode is unsupported.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- OVERRIDES ---
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
|
||||||
|
settingsManager.removeCallback(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLibDisplayModeUpdate(displayMode: DisplayMode) {
|
||||||
|
mDisplayMode = displayMode
|
||||||
|
|
||||||
|
updateLibraryData()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,10 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentLoadingBinding
|
import org.oxycblt.auxio.databinding.FragmentLoadingBinding
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragment that handles what to display during the loading process.
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
class LoadingFragment : Fragment() {
|
class LoadingFragment : Fragment() {
|
||||||
private val loadingModel: LoadingViewModel by viewModels {
|
private val loadingModel: LoadingViewModel by viewModels {
|
||||||
LoadingViewModel.Factory(requireActivity().application)
|
LoadingViewModel.Factory(requireActivity().application)
|
||||||
|
@ -85,6 +89,9 @@ class LoadingFragment : Fragment() {
|
||||||
|
|
||||||
// --- PERMISSIONS ---
|
// --- PERMISSIONS ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Auxio has the permissions to load music
|
||||||
|
*/
|
||||||
private fun hasNoPermissions(): Boolean {
|
private fun hasNoPermissions(): Boolean {
|
||||||
val needRationale = shouldShowRequestPermissionRationale(
|
val needRationale = shouldShowRequestPermissionRationale(
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
|
@ -107,16 +114,24 @@ class LoadingFragment : Fragment() {
|
||||||
|
|
||||||
// --- UI DISPLAY ---
|
// --- UI DISPLAY ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide all error elements and return to the loading view
|
||||||
|
*/
|
||||||
private fun showLoading(binding: FragmentLoadingBinding) {
|
private fun showLoading(binding: FragmentLoadingBinding) {
|
||||||
binding.apply {
|
binding.apply {
|
||||||
loadingCircle.visibility = View.VISIBLE
|
|
||||||
loadingErrorIcon.visibility = View.GONE
|
loadingErrorIcon.visibility = View.GONE
|
||||||
loadingErrorText.visibility = View.GONE
|
loadingErrorText.visibility = View.GONE
|
||||||
loadingRetryButton.visibility = View.GONE
|
loadingRetryButton.visibility = View.GONE
|
||||||
loadingGrantButton.visibility = View.GONE
|
loadingGrantButton.visibility = View.GONE
|
||||||
|
loadingCircle.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show an error prompt.
|
||||||
|
* @param error The [MusicStore.Response] that this error corresponds to. Ignores
|
||||||
|
* [MusicStore.Response.SUCCESS]
|
||||||
|
*/
|
||||||
private fun showError(binding: FragmentLoadingBinding, error: MusicStore.Response) {
|
private fun showError(binding: FragmentLoadingBinding, error: MusicStore.Response) {
|
||||||
binding.loadingCircle.visibility = View.GONE
|
binding.loadingCircle.visibility = View.GONE
|
||||||
binding.loadingErrorIcon.visibility = View.VISIBLE
|
binding.loadingErrorIcon.visibility = View.VISIBLE
|
||||||
|
|
|
@ -9,17 +9,25 @@ import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel responsible for the loading UI and beginning the loading process overall.
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
class LoadingViewModel(private val app: Application) : ViewModel() {
|
class LoadingViewModel(private val app: Application) : ViewModel() {
|
||||||
private val mResponse = MutableLiveData<MusicStore.Response?>(null)
|
private val mResponse = MutableLiveData<MusicStore.Response?>(null)
|
||||||
val response: LiveData<MusicStore.Response?> = mResponse
|
|
||||||
|
|
||||||
private val mDoGrant = MutableLiveData(false)
|
private val mDoGrant = MutableLiveData(false)
|
||||||
|
|
||||||
|
/** The last response from [MusicStore]. Null if the loading process is occurring. */
|
||||||
|
val response: LiveData<MusicStore.Response?> = mResponse
|
||||||
val doGrant: LiveData<Boolean> = mDoGrant
|
val doGrant: LiveData<Boolean> = mDoGrant
|
||||||
|
|
||||||
private var isBusy = false
|
private var isBusy = false
|
||||||
|
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin the music loading process. The response is pushed to [response]
|
||||||
|
*/
|
||||||
fun load() {
|
fun load() {
|
||||||
// Dont start a new load if the last one hasnt finished
|
// Dont start a new load if the last one hasnt finished
|
||||||
if (isBusy) return
|
if (isBusy) return
|
||||||
|
@ -33,14 +41,23 @@ class LoadingViewModel(private val app: Application) : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the grant prompt.
|
||||||
|
*/
|
||||||
fun grant() {
|
fun grant() {
|
||||||
mDoGrant.value = true
|
mDoGrant.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark that the grant prompt is now shown.
|
||||||
|
*/
|
||||||
fun doneWithGrant() {
|
fun doneWithGrant() {
|
||||||
mDoGrant.value = false
|
mDoGrant.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify this viewmodel that there are no permissions
|
||||||
|
*/
|
||||||
fun notifyNoPermissions() {
|
fun notifyNoPermissions() {
|
||||||
mResponse.value = MusicStore.Response.NO_PERMS
|
mResponse.value = MusicStore.Response.NO_PERMS
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import android.net.Uri
|
||||||
* The base data object for all music.
|
* The base data object for all music.
|
||||||
* @property id The ID that is assigned to this object
|
* @property id The ID that is assigned to this object
|
||||||
* @property name The name of this object (Such as a song title)
|
* @property name The name of this object (Such as a song title)
|
||||||
* @author OxygenCobalt
|
|
||||||
*/
|
*/
|
||||||
sealed class BaseModel {
|
sealed class BaseModel {
|
||||||
abstract val id: Long
|
abstract val id: Long
|
||||||
|
@ -34,7 +33,6 @@ sealed class Parent : BaseModel() {
|
||||||
* @property genre The Song's [Genre]
|
* @property genre The Song's [Genre]
|
||||||
* @property seconds The Song's duration in seconds
|
* @property seconds The Song's duration in seconds
|
||||||
* @property formattedDuration The Song's duration as a duration string.
|
* @property formattedDuration The Song's duration as a duration string.
|
||||||
* @author OxygenCobalt
|
|
||||||
*/
|
*/
|
||||||
data class Song(
|
data class Song(
|
||||||
override val id: Long = -1,
|
override val id: Long = -1,
|
||||||
|
@ -74,7 +72,6 @@ data class Song(
|
||||||
* @property artist The Album's parent [Artist]. use this instead of [artistName]
|
* @property artist The Album's parent [Artist]. use this instead of [artistName]
|
||||||
* @property songs The Album's child [Song]s.
|
* @property songs The Album's child [Song]s.
|
||||||
* @property totalDuration The combined duration of all of the album's child songs, formatted.
|
* @property totalDuration The combined duration of all of the album's child songs, formatted.
|
||||||
* @author OxygenCobalt
|
|
||||||
*/
|
*/
|
||||||
data class Album(
|
data class Album(
|
||||||
override val id: Long = -1,
|
override val id: Long = -1,
|
||||||
|
@ -108,7 +105,6 @@ data class Album(
|
||||||
* @property albums The list of all [Album]s in this artist
|
* @property albums The list of all [Album]s in this artist
|
||||||
* @property genre The most prominent genre for this artist
|
* @property genre The most prominent genre for this artist
|
||||||
* @property songs The list of all [Song]s in this artist
|
* @property songs The list of all [Song]s in this artist
|
||||||
* @author OxygenCobalt
|
|
||||||
*/
|
*/
|
||||||
data class Artist(
|
data class Artist(
|
||||||
override val id: Long = -1,
|
override val id: Long = -1,
|
||||||
|
@ -134,14 +130,13 @@ data class Artist(
|
||||||
* The data object for a genre. Inherits [Parent]
|
* The data object for a genre. Inherits [Parent]
|
||||||
* @property songs The list of all [Song]s in this genre.
|
* @property songs The list of all [Song]s in this genre.
|
||||||
* @property resolvedName A name that has been resolved from its int-genre form to its named form.
|
* @property resolvedName A name that has been resolved from its int-genre form to its named form.
|
||||||
* @author OxygenCobalt
|
|
||||||
*/
|
*/
|
||||||
data class Genre(
|
data class Genre(
|
||||||
override val id: Long = -1,
|
override val id: Long = -1,
|
||||||
override val name: String,
|
override val name: String,
|
||||||
) : Parent() {
|
) : Parent() {
|
||||||
val resolvedName: String by lazy {
|
val resolvedName: String by lazy {
|
||||||
if (name.contains(Regex("[0123456789)]"))) {
|
if (name.contains(Regex("([1-9])"))) {
|
||||||
name.toNamedGenre() ?: name
|
name.toNamedGenre() ?: name
|
||||||
} else {
|
} else {
|
||||||
name
|
name
|
||||||
|
@ -162,7 +157,6 @@ data class Genre(
|
||||||
/**
|
/**
|
||||||
* A data object used solely for the "Header" UI element. Inherits [BaseModel].
|
* A data object used solely for the "Header" UI element. Inherits [BaseModel].
|
||||||
* @property isAction Value that marks whether this header should have an action attached to it.
|
* @property isAction Value that marks whether this header should have an action attached to it.
|
||||||
* @author OxygenCobalt
|
|
||||||
*/
|
*/
|
||||||
data class Header(
|
data class Header(
|
||||||
override val id: Long = -1,
|
override val id: Long = -1,
|
||||||
|
|
|
@ -38,12 +38,12 @@ class MusicStore private constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Marker for whether the music loading process has successfully completed. */
|
||||||
var loaded = false
|
var loaded = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load/Sort the entire music library.
|
* Load/Sort the entire music library. Should always be ran on a coroutine.
|
||||||
* ***THIS SHOULD ONLY BE RAN FROM AN IO THREAD.***
|
|
||||||
* @param app [Application] required to load the music.
|
* @param app [Application] required to load the music.
|
||||||
*/
|
*/
|
||||||
suspend fun load(app: Application): Response {
|
suspend fun load(app: Application): Response {
|
||||||
|
@ -68,8 +68,6 @@ class MusicStore private constructor() {
|
||||||
mArtists = linker.artists.toList()
|
mArtists = linker.artists.toList()
|
||||||
mGenres = linker.genres.toList()
|
mGenres = linker.genres.toList()
|
||||||
|
|
||||||
loaded = true
|
|
||||||
|
|
||||||
this@MusicStore.logD(
|
this@MusicStore.logD(
|
||||||
"Music load completed successfully in ${System.currentTimeMillis() - start}ms."
|
"Music load completed successfully in ${System.currentTimeMillis() - start}ms."
|
||||||
)
|
)
|
||||||
|
@ -80,12 +78,15 @@ class MusicStore private constructor() {
|
||||||
return@withContext Response.FAILED
|
return@withContext Response.FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loaded = true
|
||||||
|
|
||||||
return@withContext Response.SUCCESS
|
return@withContext Response.SUCCESS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the song for a specific URI.
|
* Get the song for a file [uri].
|
||||||
|
* @return The corresponding [Song] for this [uri], null if there isnt one.
|
||||||
*/
|
*/
|
||||||
suspend fun getSongForUri(uri: Uri, resolver: ContentResolver): Song? {
|
suspend fun getSongForUri(uri: Uri, resolver: ContentResolver): Song? {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
|
@ -98,6 +99,9 @@ class MusicStore private constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responses that [MusicStore] sends back when a [load] call completes.
|
||||||
|
*/
|
||||||
enum class Response {
|
enum class Response {
|
||||||
NO_MUSIC, NO_PERMS, FAILED, SUCCESS
|
NO_MUSIC, NO_PERMS, FAILED, SUCCESS
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,25 +60,17 @@ fun String.toNamedGenre(): String? {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a song id to its URI
|
* Convert an id to its corresponding URI
|
||||||
* @return The [Uri] for this song/
|
|
||||||
*/
|
*/
|
||||||
fun Long.toURI(): Uri {
|
fun Long.toURI(): Uri {
|
||||||
return ContentUris.withAppendedId(
|
return ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, this)
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
|
||||||
this
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the URI for an album's cover art.
|
* Get the URI for an album's cover art, corresponds to MediaStore.
|
||||||
* @return The [Uri] for the album's cover art/
|
|
||||||
*/
|
*/
|
||||||
fun Long.toAlbumArtURI(): Uri {
|
fun Long.toAlbumArtURI(): Uri {
|
||||||
return ContentUris.withAppendedId(
|
return ContentUris.withAppendedId(Uri.parse("content://media/external/audio/albumart"), this)
|
||||||
Uri.parse("content://media/external/audio/albumart"),
|
|
||||||
this
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -10,7 +10,9 @@ import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object that links music data to one-another,
|
* Object that links music data, such as grouping songs into their albums & genres and creating
|
||||||
|
* artists out of the albums.
|
||||||
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class MusicLinker(
|
class MusicLinker(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
@ -21,6 +23,10 @@ class MusicLinker(
|
||||||
private val resolver = context.contentResolver
|
private val resolver = context.contentResolver
|
||||||
val artists = mutableListOf<Artist>()
|
val artists = mutableListOf<Artist>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin the linking process.
|
||||||
|
* Modified models are pushed to [songs], [albums], [artists], and [genres]
|
||||||
|
*/
|
||||||
fun link() {
|
fun link() {
|
||||||
linkAlbums()
|
linkAlbums()
|
||||||
linkArtists()
|
linkArtists()
|
||||||
|
|
|
@ -17,6 +17,7 @@ import org.oxycblt.auxio.music.toAlbumArtURI
|
||||||
/**
|
/**
|
||||||
* Class that loads/constructs [Genre]s, [Album]s, and [Song] objects from the filesystem
|
* Class that loads/constructs [Genre]s, [Album]s, and [Song] objects from the filesystem
|
||||||
* Artists are constructed in [MusicLinker], as they are only really containers for [Album]s
|
* Artists are constructed in [MusicLinker], as they are only really containers for [Album]s
|
||||||
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class MusicLoader(private val app: Application) {
|
class MusicLoader(private val app: Application) {
|
||||||
var genres = mutableListOf<Genre>()
|
var genres = mutableListOf<Genre>()
|
||||||
|
@ -25,6 +26,9 @@ class MusicLoader(private val app: Application) {
|
||||||
|
|
||||||
private val resolver = app.contentResolver
|
private val resolver = app.contentResolver
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin the loading process. Resulting models are pushed to [genres], [albums], and [songs].
|
||||||
|
*/
|
||||||
fun loadMusic() {
|
fun loadMusic() {
|
||||||
loadGenres()
|
loadGenres()
|
||||||
loadAlbums()
|
loadAlbums()
|
||||||
|
|
|
@ -12,6 +12,7 @@ import org.oxycblt.auxio.ui.toAnimDrawable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom [AppCompatImageButton] that handles the animated play/pause icons.
|
* Custom [AppCompatImageButton] that handles the animated play/pause icons.
|
||||||
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class PlayPauseButton @JvmOverloads constructor(
|
class PlayPauseButton @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
@ -27,6 +28,10 @@ class PlayPauseButton @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the play/pause icon to reflect [isPlaying]
|
||||||
|
* @param animate Whether the icon change should be animated or not.
|
||||||
|
*/
|
||||||
fun setPlaying(isPlaying: Boolean, animate: Boolean) {
|
fun setPlaying(isPlaying: Boolean, animate: Boolean) {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
if (animate) {
|
if (animate) {
|
||||||
|
|
|
@ -121,7 +121,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
playbackManager.playSong(song, mode)
|
playbackManager.playSong(song, mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Play an album.*/
|
/**
|
||||||
|
* Play an [album].
|
||||||
|
* @param shuffled Whether to shuffle the new queue
|
||||||
|
*/
|
||||||
fun playAlbum(album: Album, shuffled: Boolean) {
|
fun playAlbum(album: Album, shuffled: Boolean) {
|
||||||
if (album.songs.isEmpty()) {
|
if (album.songs.isEmpty()) {
|
||||||
logE("Album is empty, Not playing.")
|
logE("Album is empty, Not playing.")
|
||||||
|
@ -132,7 +135,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
playbackManager.playParent(album, shuffled)
|
playbackManager.playParent(album, shuffled)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Play an Artist */
|
/**
|
||||||
|
* Play an [artist].
|
||||||
|
* @param shuffled Whether to shuffle the new queue
|
||||||
|
*/
|
||||||
fun playArtist(artist: Artist, shuffled: Boolean) {
|
fun playArtist(artist: Artist, shuffled: Boolean) {
|
||||||
if (artist.songs.isEmpty()) {
|
if (artist.songs.isEmpty()) {
|
||||||
logE("Artist is empty, Not playing.")
|
logE("Artist is empty, Not playing.")
|
||||||
|
@ -143,7 +149,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
playbackManager.playParent(artist, shuffled)
|
playbackManager.playParent(artist, shuffled)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Play a genre. */
|
/**
|
||||||
|
* Play a [genre].
|
||||||
|
* @param shuffled Whether to shuffle the new queue
|
||||||
|
*/
|
||||||
fun playGenre(genre: Genre, shuffled: Boolean) {
|
fun playGenre(genre: Genre, shuffled: Boolean) {
|
||||||
if (genre.songs.isEmpty()) {
|
if (genre.songs.isEmpty()) {
|
||||||
logE("Genre is empty, Not playing.")
|
logE("Genre is empty, Not playing.")
|
||||||
|
@ -154,7 +163,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
playbackManager.playParent(genre, shuffled)
|
playbackManager.playParent(genre, shuffled)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Play using a file URI. This will not play instantly during the initial startup sequence.*/
|
/**
|
||||||
|
* Play using a file [uri].
|
||||||
|
* This will not play instantly during the initial startup sequence.
|
||||||
|
*/
|
||||||
fun playWithUri(uri: Uri, context: Context) {
|
fun playWithUri(uri: Uri, context: Context) {
|
||||||
// Check if everything is already running to run the URI play
|
// Check if everything is already running to run the URI play
|
||||||
if (playbackManager.isRestored && musicStore.loaded) {
|
if (playbackManager.isRestored && musicStore.loaded) {
|
||||||
|
@ -164,7 +176,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Actually play with a given URI internally. The frontend doesn't play instantly. */
|
/**
|
||||||
|
* Play with a file URI.
|
||||||
|
* This is called after [playWithUri] once its deemed safe to do so.
|
||||||
|
*/
|
||||||
private fun playWithUriInternal(uri: Uri, context: Context) {
|
private fun playWithUriInternal(uri: Uri, context: Context) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
musicStore.getSongForUri(uri, context.contentResolver)?.let { song ->
|
musicStore.getSongForUri(uri, context.contentResolver)?.let { song ->
|
||||||
|
@ -173,14 +188,18 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shuffle all songs */
|
/**
|
||||||
|
* Shuffle all songs
|
||||||
|
*/
|
||||||
fun shuffleAll() {
|
fun shuffleAll() {
|
||||||
playbackManager.shuffleAll()
|
playbackManager.shuffleAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- POSITION FUNCTIONS ---
|
// --- POSITION FUNCTIONS ---
|
||||||
|
|
||||||
/** Update the position and push it to [PlaybackStateManager] */
|
/**
|
||||||
|
* Update the position and push it to [PlaybackStateManager]
|
||||||
|
*/
|
||||||
fun setPosition(progress: Int) {
|
fun setPosition(progress: Int) {
|
||||||
playbackManager.seekTo((progress * 1000).toLong())
|
playbackManager.seekTo((progress * 1000).toLong())
|
||||||
}
|
}
|
||||||
|
@ -196,22 +215,25 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
|
|
||||||
// --- QUEUE FUNCTIONS ---
|
// --- QUEUE FUNCTIONS ---
|
||||||
|
|
||||||
/** Skip to the next song. */
|
/**
|
||||||
|
* Skip to the next song.
|
||||||
|
*/
|
||||||
fun skipNext() {
|
fun skipNext() {
|
||||||
playbackManager.next()
|
playbackManager.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Skip to the previous song */
|
/**
|
||||||
|
* Skip to the previous song.
|
||||||
|
*/
|
||||||
fun skipPrev() {
|
fun skipPrev() {
|
||||||
playbackManager.prev()
|
playbackManager.prev()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a queue OR user queue item, given a QueueAdapter index.
|
* Remove an item at [adapterIndex], being a non-header data index.
|
||||||
* @param adapterIndex The [QueueAdapter] index to remove
|
* @param queueAdapter [QueueAdapter] instance to push changes to when successful.
|
||||||
* @param queueAdapter The [QueueAdapter] itself to push changes to when successful.
|
|
||||||
*/
|
*/
|
||||||
fun removeQueueAdapterItem(adapterIndex: Int, queueAdapter: QueueAdapter) {
|
fun removeQueueDataItem(adapterIndex: Int, queueAdapter: QueueAdapter) {
|
||||||
var index = adapterIndex.dec()
|
var index = adapterIndex.dec()
|
||||||
|
|
||||||
// If the item is in the user queue, then remove it from there after accounting for the header.
|
// If the item is in the user queue, then remove it from there after accounting for the header.
|
||||||
|
@ -234,12 +256,11 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move queue OR user queue items, given QueueAdapter indices.
|
* Move a queue OR user queue item from [adapterFrom] to [adapterTo], as long as both
|
||||||
* @param adapterFrom The [QueueAdapter] index that needs to be moved
|
* indices are non-header data indices.
|
||||||
* @param adapterTo The destination [QueueAdapter] index.
|
* @param queueAdapter [QueueAdapter] instance to push changes to when successful.
|
||||||
* @param queueAdapter the [QueueAdapter] to push changes to when successful.
|
|
||||||
*/
|
*/
|
||||||
fun moveQueueAdapterItems(
|
fun moveQueueDataItems(
|
||||||
adapterFrom: Int,
|
adapterFrom: Int,
|
||||||
adapterTo: Int,
|
adapterTo: Int,
|
||||||
queueAdapter: QueueAdapter
|
queueAdapter: QueueAdapter
|
||||||
|
@ -283,38 +304,50 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add a [Song] to the user queue.*/
|
/**
|
||||||
|
* Add a [Song] to the user queue.
|
||||||
|
*/
|
||||||
fun addToUserQueue(song: Song) {
|
fun addToUserQueue(song: Song) {
|
||||||
playbackManager.addToUserQueue(song)
|
playbackManager.addToUserQueue(song)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add an [Album] to the user queue */
|
/**
|
||||||
|
* Add an [Album] to the user queue
|
||||||
|
*/
|
||||||
fun addToUserQueue(album: Album) {
|
fun addToUserQueue(album: Album) {
|
||||||
val songs = SortMode.NUMERIC_DOWN.getSortedSongList(album.songs)
|
val songs = SortMode.NUMERIC_DOWN.getSortedSongList(album.songs)
|
||||||
|
|
||||||
playbackManager.addToUserQueue(songs)
|
playbackManager.addToUserQueue(songs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clear the user queue entirely */
|
/**
|
||||||
|
* Clear the user queue entirely
|
||||||
|
*/
|
||||||
fun clearUserQueue() {
|
fun clearUserQueue() {
|
||||||
playbackManager.clearUserQueue()
|
playbackManager.clearUserQueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- STATUS FUNCTIONS ---
|
// --- STATUS FUNCTIONS ---
|
||||||
|
|
||||||
/** Flip the playing status, e.g from playing to paused */
|
/**
|
||||||
|
* Flip the playing status, e.g from playing to paused
|
||||||
|
*/
|
||||||
fun invertPlayingStatus() {
|
fun invertPlayingStatus() {
|
||||||
enableAnimation()
|
enableAnimation()
|
||||||
|
|
||||||
playbackManager.setPlaying(!playbackManager.isPlaying)
|
playbackManager.setPlaying(!playbackManager.isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Flip the shuffle status, e.g from on to off. Will keep song by default. */
|
/**
|
||||||
|
* Flip the shuffle status, e.g from on to off. Will keep song by default.
|
||||||
|
*/
|
||||||
fun invertShuffleStatus() {
|
fun invertShuffleStatus() {
|
||||||
playbackManager.setShuffling(!playbackManager.isShuffling, keepSong = true)
|
playbackManager.setShuffling(!playbackManager.isShuffling, keepSong = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Increment the loop status, e.g from off to loop once */
|
/**
|
||||||
|
* Increment the loop status, e.g from off to loop once
|
||||||
|
*/
|
||||||
fun incrementLoopStatus() {
|
fun incrementLoopStatus() {
|
||||||
playbackManager.setLoopMode(playbackManager.loopMode.increment())
|
playbackManager.setLoopMode(playbackManager.loopMode.increment())
|
||||||
}
|
}
|
||||||
|
@ -322,8 +355,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
// --- SAVE/RESTORE FUNCTIONS ---
|
// --- SAVE/RESTORE FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force save the current [PlaybackStateManager] state to the database. Called by SettingsListFragment.
|
* Force save the current [PlaybackStateManager] state to the database.
|
||||||
* @param context [Context] required.
|
* Called by SettingsListFragment.
|
||||||
*/
|
*/
|
||||||
fun savePlaybackState(context: Context) {
|
fun savePlaybackState(context: Context) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
@ -334,7 +367,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the file last-saved file intent, or restore playback, depending on the situation.
|
* Restore playback on startup. This can do one of two things:
|
||||||
|
* - Play a file intent that was given by MainActivity in [playWithUri]
|
||||||
|
* - 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 = mIntentUri
|
||||||
|
@ -347,13 +382,17 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
// 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
|
mIntentUri = null
|
||||||
} else if (!playbackManager.isRestored) {
|
} else if (!playbackManager.isRestored) {
|
||||||
|
// Otherwise just restore
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
playbackManager.getStateFromDatabase(context)
|
playbackManager.restoreFromDatabase(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Attempt to restore the current playback state from an existing [PlaybackStateManager] instance */
|
/**
|
||||||
|
* Attempt to restore the current playback state from an existing
|
||||||
|
* [PlaybackStateManager] instance.
|
||||||
|
*/
|
||||||
private fun restorePlaybackState() {
|
private fun restorePlaybackState() {
|
||||||
logD("Attempting to restore playback state.")
|
logD("Attempting to restore playback state.")
|
||||||
|
|
||||||
|
@ -371,17 +410,23 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
|
|
||||||
// --- OTHER FUNCTIONS ---
|
// --- OTHER FUNCTIONS ---
|
||||||
|
|
||||||
/** Set whether the seeking indicator should be highlighted */
|
/**
|
||||||
fun setSeekingStatus(value: Boolean) {
|
* Set whether the seeking indicator should be highlighted
|
||||||
mIsSeeking.value = value
|
*/
|
||||||
|
fun setSeekingStatus(isSeeking: Boolean) {
|
||||||
|
mIsSeeking.value = isSeeking
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Enable animation on CompactPlaybackFragment */
|
/**
|
||||||
|
* Enable animation on CompactPlaybackFragment
|
||||||
|
*/
|
||||||
fun enableAnimation() {
|
fun enableAnimation() {
|
||||||
mCanAnimate = true
|
mCanAnimate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Disable animation on CompactPlaybackFragment */
|
/**
|
||||||
|
* Disable animation on CompactPlaybackFragment
|
||||||
|
*/
|
||||||
fun disableAnimation() {
|
fun disableAnimation() {
|
||||||
mCanAnimate = false
|
mCanAnimate = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,14 +38,16 @@ class QueueAdapter(
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
val item = data[position]
|
val item = data[position]
|
||||||
|
|
||||||
return if (item is Header)
|
return if (item is Header) {
|
||||||
if (item.isAction)
|
if (item.isAction) {
|
||||||
USER_QUEUE_HEADER_ITEM_TYPE
|
USER_QUEUE_HEADER_ITEM_TYPE
|
||||||
else
|
} else {
|
||||||
HeaderViewHolder.ITEM_TYPE
|
HeaderViewHolder.ITEM_TYPE
|
||||||
else
|
}
|
||||||
|
} else {
|
||||||
QUEUE_SONG_ITEM_TYPE
|
QUEUE_SONG_ITEM_TYPE
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
|
@ -80,7 +82,8 @@ class QueueAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit data using [AsyncListDiffer]. **Only use this if you have no idea what changes occurred to the data**
|
* Submit data using [AsyncListDiffer].
|
||||||
|
* **Only use this if you have no idea what changes occurred to the data**
|
||||||
*/
|
*/
|
||||||
fun submitList(newData: MutableList<BaseModel>) {
|
fun submitList(newData: MutableList<BaseModel>) {
|
||||||
if (data != newData) {
|
if (data != newData) {
|
||||||
|
@ -91,7 +94,8 @@ class QueueAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move Items. Used since [submitList] will cause QueueAdapter to freak-out here.
|
* Move Items.
|
||||||
|
* Used since [submitList] will cause QueueAdapter to freak-out here.
|
||||||
*/
|
*/
|
||||||
fun moveItems(adapterFrom: Int, adapterTo: Int) {
|
fun moveItems(adapterFrom: Int, adapterTo: Int) {
|
||||||
val item = data.removeAt(adapterFrom)
|
val item = data.removeAt(adapterFrom)
|
||||||
|
@ -101,17 +105,20 @@ class QueueAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove an item. Used since [submitList] will cause QueueAdapter to freak-out here.
|
* Remove an item.
|
||||||
|
* Used since [submitList] will cause QueueAdapter to freak-out here.
|
||||||
*/
|
*/
|
||||||
fun removeItem(adapterIndex: Int) {
|
fun removeItem(adapterIndex: Int) {
|
||||||
data.removeAt(adapterIndex)
|
data.removeAt(adapterIndex)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Check for two things:
|
* If the data from the next queue is now entirely empty [Signified by a header at the
|
||||||
* If the data from the next queue is now entirely empty [Signified by a header at the end]
|
* end, remove the next queue header as notify as such.
|
||||||
* Or if the data from the last queue is now entirely empty [Signified by there being
|
*
|
||||||
* 2 headers with no items in between]
|
* If the user queue is empty [Signified by there being two headers at the beginning with
|
||||||
* If so, remove the header and the removed item in a range. Otherwise just remove the item.
|
* nothing in between], then remove the user queue header and notify as such.
|
||||||
|
*
|
||||||
|
* Otherwise just remove the item as usual.
|
||||||
*/
|
*/
|
||||||
if (data[data.lastIndex] is Header) {
|
if (data[data.lastIndex] is Header) {
|
||||||
val lastIndex = data.lastIndex
|
val lastIndex = data.lastIndex
|
||||||
|
|
|
@ -11,7 +11,6 @@ import kotlin.math.sign
|
||||||
/**
|
/**
|
||||||
* The Drag callback used by the queue recyclerview. Delivers updates to [PlaybackViewModel]
|
* The Drag callback used by the queue recyclerview. Delivers updates to [PlaybackViewModel]
|
||||||
* and [QueueAdapter] simultaneously.
|
* and [QueueAdapter] simultaneously.
|
||||||
* @param playbackModel The [PlaybackViewModel] required to dispatch updates to.
|
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouchHelper.Callback() {
|
class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouchHelper.Callback() {
|
||||||
|
@ -58,7 +57,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
||||||
viewHolder: RecyclerView.ViewHolder,
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
target: RecyclerView.ViewHolder
|
target: RecyclerView.ViewHolder
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return playbackModel.moveQueueAdapterItems(
|
return playbackModel.moveQueueDataItems(
|
||||||
viewHolder.adapterPosition,
|
viewHolder.adapterPosition,
|
||||||
target.adapterPosition,
|
target.adapterPosition,
|
||||||
queueAdapter
|
queueAdapter
|
||||||
|
@ -66,7 +65,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||||
playbackModel.removeQueueAdapterItem(viewHolder.adapterPosition, queueAdapter)
|
playbackModel.removeQueueDataItem(viewHolder.adapterPosition, queueAdapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -22,8 +22,6 @@ import org.oxycblt.auxio.ui.isIrregularLandscape
|
||||||
/**
|
/**
|
||||||
* A [Fragment] that contains both the user queue and the next queue, with the ability to
|
* A [Fragment] that contains both the user queue and the next queue, with the ability to
|
||||||
* edit them as well.
|
* edit them as well.
|
||||||
*
|
|
||||||
* Instantiation is done by the navigation component, **do not instantiate this fragment manually.**
|
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class QueueFragment : Fragment() {
|
class QueueFragment : Fragment() {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.oxycblt.auxio.playback.state
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum that determines the playback repeat mode.
|
* Enum that determines the playback repeat mode.
|
||||||
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
enum class LoopMode {
|
enum class LoopMode {
|
||||||
NONE, ONCE, INFINITE;
|
NONE, ONCE, INFINITE;
|
||||||
|
@ -35,7 +36,7 @@ enum class LoopMode {
|
||||||
const val CONSTANT_INFINITE = 0xA052
|
const val CONSTANT_INFINITE = 0xA052
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert an int constant into a LoopMode
|
* Convert an int [constant] into a LoopMode
|
||||||
* @return The corresponding LoopMode. Null if it corresponds to nothing.
|
* @return The corresponding LoopMode. Null if it corresponds to nothing.
|
||||||
*/
|
*/
|
||||||
fun fromInt(constant: Int): LoopMode? {
|
fun fromInt(constant: Int): LoopMode? {
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package org.oxycblt.auxio.playback.state
|
package org.oxycblt.auxio.playback.state
|
||||||
|
|
||||||
// Enum that instructs how the queue should be constructed
|
/**
|
||||||
|
* Enum that indicates how the queue should be constructed.
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
enum class PlaybackMode {
|
enum class PlaybackMode {
|
||||||
/** Construct the queue from the genre's songs */
|
/** Construct the queue from the genre's songs */
|
||||||
IN_GENRE,
|
IN_GENRE,
|
||||||
|
@ -31,7 +34,7 @@ enum class PlaybackMode {
|
||||||
const val CONSTANT_ALL_SONGS = 0xA043
|
const val CONSTANT_ALL_SONGS = 0xA043
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a [PlaybackMode] for an int constant
|
* Get a [PlaybackMode] for an int [constant]
|
||||||
* @return The mode, null if there isnt one for this.
|
* @return The mode, null if there isnt one for this.
|
||||||
*/
|
*/
|
||||||
fun fromInt(constant: Int): PlaybackMode? {
|
fun fromInt(constant: Int): PlaybackMode? {
|
||||||
|
|
|
@ -144,8 +144,7 @@ class PlaybackStateManager private constructor() {
|
||||||
// --- PLAYING FUNCTIONS ---
|
// --- PLAYING FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play a song.
|
* Play a [song].
|
||||||
* @param song The song to be played
|
|
||||||
* @param mode The [PlaybackMode] to construct the queue off of.
|
* @param mode The [PlaybackMode] to construct the queue off of.
|
||||||
*/
|
*/
|
||||||
fun playSong(song: Song, mode: PlaybackMode) {
|
fun playSong(song: Song, mode: PlaybackMode) {
|
||||||
|
@ -190,9 +189,8 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play a parent model, e.g an artist or an album.
|
* Play a [parent], such as an artist or album.
|
||||||
* @param parent The model to use
|
* @param shuffled Whether the queue is shuffled or not
|
||||||
* @param shuffled Whether to shuffle the queue or not
|
|
||||||
*/
|
*/
|
||||||
fun playParent(parent: Parent, shuffled: Boolean) {
|
fun playParent(parent: Parent, shuffled: Boolean) {
|
||||||
logD("Playing ${parent.name}")
|
logD("Playing ${parent.name}")
|
||||||
|
@ -221,8 +219,19 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut function for updating what song is being played. ***USE THIS INSTEAD OF WRITING OUT ALL THE CODE YOURSELF!!!***
|
* Shuffle all songs.
|
||||||
* @param song The song to play
|
*/
|
||||||
|
fun shuffleAll() {
|
||||||
|
mMode = PlaybackMode.ALL_SONGS
|
||||||
|
mQueue = musicStore.songs.toMutableList()
|
||||||
|
mParent = null
|
||||||
|
|
||||||
|
setShuffling(true, keepSong = false)
|
||||||
|
updatePlayback(mQueue[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the playback to a new [song], doing all the required logic.
|
||||||
*/
|
*/
|
||||||
private fun updatePlayback(song: Song) {
|
private fun updatePlayback(song: Song) {
|
||||||
mIsInUserQueue = false
|
mIsInUserQueue = false
|
||||||
|
@ -326,8 +335,7 @@ class PlaybackStateManager private constructor() {
|
||||||
// --- QUEUE EDITING FUNCTIONS ---
|
// --- QUEUE EDITING FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a queue item at a QUEUE index. Will log an error if the index is out of bounds
|
* Remove a queue item at [index]. Will ignore invalid indexes.
|
||||||
* @param index The index at which the item should be removed.
|
|
||||||
*/
|
*/
|
||||||
fun removeQueueItem(index: Int): Boolean {
|
fun removeQueueItem(index: Int): Boolean {
|
||||||
logD("Removing item ${mQueue[index].name}.")
|
logD("Removing item ${mQueue[index].name}.")
|
||||||
|
@ -346,10 +354,7 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move a queue item from a QUEUE INDEX to a QUEUE INDEX. Will log an error if one of the indices
|
* Move a queue item at [from] to a position at [to]. Will ignore invalid indexes.
|
||||||
* is out of bounds.
|
|
||||||
* @param from The starting item's index
|
|
||||||
* @param to The destination index.
|
|
||||||
*/
|
*/
|
||||||
fun moveQueueItems(from: Int, to: Int): Boolean {
|
fun moveQueueItems(from: Int, to: Int): Boolean {
|
||||||
if (from > mQueue.size || from < 0 || to > mQueue.size || to < 0) {
|
if (from > mQueue.size || from < 0 || to > mQueue.size || to < 0) {
|
||||||
|
@ -367,8 +372,7 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a song to the user queue.
|
* Add a [song] to the user queue.
|
||||||
* @param song The song to add
|
|
||||||
*/
|
*/
|
||||||
fun addToUserQueue(song: Song) {
|
fun addToUserQueue(song: Song) {
|
||||||
mUserQueue.add(song)
|
mUserQueue.add(song)
|
||||||
|
@ -377,8 +381,7 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a list of songs to the user queue.
|
* Add a list of [songs] to the user queue.
|
||||||
* @param songs The songs to add.
|
|
||||||
*/
|
*/
|
||||||
fun addToUserQueue(songs: List<Song>) {
|
fun addToUserQueue(songs: List<Song>) {
|
||||||
mUserQueue.addAll(songs)
|
mUserQueue.addAll(songs)
|
||||||
|
@ -387,8 +390,7 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a USER QUEUE item at a USER QUEUE index. Will log an error if the index is out of bounds.
|
* Remove a USER queue item at [index]. Will ignore invalid indexes.
|
||||||
* @param index The index at which the item should be removed.
|
|
||||||
*/
|
*/
|
||||||
fun removeUserQueueItem(index: Int) {
|
fun removeUserQueueItem(index: Int) {
|
||||||
logD("Removing item ${mUserQueue[index].name}.")
|
logD("Removing item ${mUserQueue[index].name}.")
|
||||||
|
@ -405,10 +407,7 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move a USER QUEUE item from a USER QUEUE index to another USER QUEUE index. Will log an error if one of the indices
|
* Move a USER queue item at [from] to a position at [to]. Will ignore invalid indexes.
|
||||||
* is out of bounds.
|
|
||||||
* @param from The starting item's index
|
|
||||||
* @param to The destination index.
|
|
||||||
*/
|
*/
|
||||||
fun moveUserQueueItems(from: Int, to: Int) {
|
fun moveUserQueueItems(from: Int, to: Int) {
|
||||||
if (from > mUserQueue.size || from < 0 || to > mUserQueue.size || to < 0) {
|
if (from > mUserQueue.size || from < 0 || to > mUserQueue.size || to < 0) {
|
||||||
|
@ -449,24 +448,11 @@ class PlaybackStateManager private constructor() {
|
||||||
// --- SHUFFLE FUNCTIONS ---
|
// --- SHUFFLE FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shuffle all songs.
|
* Set whether this instance is [shuffled]. Updates the queue accordingly
|
||||||
*/
|
|
||||||
fun shuffleAll() {
|
|
||||||
mMode = PlaybackMode.ALL_SONGS
|
|
||||||
mQueue = musicStore.songs.toMutableList()
|
|
||||||
mParent = null
|
|
||||||
|
|
||||||
setShuffling(true, keepSong = false)
|
|
||||||
updatePlayback(mQueue[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the shuffle status. Updates the queue accordingly
|
|
||||||
* @param shuffling Whether the queue should be shuffled or not.
|
|
||||||
* @param keepSong Whether the current song should be kept as the queue is shuffled/unshuffled
|
* @param keepSong Whether the current song should be kept as the queue is shuffled/unshuffled
|
||||||
*/
|
*/
|
||||||
fun setShuffling(shuffling: Boolean, keepSong: Boolean) {
|
fun setShuffling(shuffled: Boolean, keepSong: Boolean) {
|
||||||
mIsShuffling = shuffling
|
mIsShuffling = shuffled
|
||||||
|
|
||||||
if (mIsShuffling) {
|
if (mIsShuffling) {
|
||||||
genShuffle(keepSong, mIsInUserQueue)
|
genShuffle(keepSong, mIsInUserQueue)
|
||||||
|
@ -525,8 +511,7 @@ class PlaybackStateManager private constructor() {
|
||||||
// --- STATE FUNCTIONS ---
|
// --- STATE FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the current playing status
|
* Set whether this instance is currently [playing].
|
||||||
* @param playing Whether the playback should be playing or paused.
|
|
||||||
*/
|
*/
|
||||||
fun setPlaying(playing: Boolean) {
|
fun setPlaying(playing: Boolean) {
|
||||||
if (mIsPlaying != playing) {
|
if (mIsPlaying != playing) {
|
||||||
|
@ -539,7 +524,7 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the current position. Will not notify any listeners of a seek event, that's what [seekTo] is for.
|
* Update the current [position]. Will not notify listeners of a seek event.
|
||||||
* @param position The new position in millis.
|
* @param position The new position in millis.
|
||||||
* @see seekTo
|
* @see seekTo
|
||||||
*/
|
*/
|
||||||
|
@ -553,10 +538,9 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* **Seek** to a position, this calls [PlaybackStateManager.Callback.onSeek] to notify
|
* **Seek** to a [position], this calls [PlaybackStateManager.Callback.onSeek] to notify
|
||||||
* elements that rely on that.
|
* elements that rely on that.
|
||||||
* @param position The position to seek to in millis.
|
* @param position The position to seek to in millis.
|
||||||
* @see setPosition
|
|
||||||
*/
|
*/
|
||||||
fun seekTo(position: Long) {
|
fun seekTo(position: Long) {
|
||||||
mPosition = position
|
mPosition = position
|
||||||
|
@ -573,15 +557,14 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the [LoopMode]
|
* Set the [LoopMode] to [mode].
|
||||||
* @param mode The [LoopMode] to be used
|
|
||||||
*/
|
*/
|
||||||
fun setLoopMode(mode: LoopMode) {
|
fun setLoopMode(mode: LoopMode) {
|
||||||
mLoopMode = mode
|
mLoopMode = mode
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the current [LoopMode], if needed.
|
* Reset the current [LoopMode] from [LoopMode.ONCE], if needed.
|
||||||
* Use this instead of duplicating the code manually.
|
* Use this instead of duplicating the code manually.
|
||||||
*/
|
*/
|
||||||
fun clearLoopMode() {
|
fun clearLoopMode() {
|
||||||
|
@ -614,45 +597,42 @@ class PlaybackStateManager private constructor() {
|
||||||
suspend fun saveStateToDatabase(context: Context) {
|
suspend fun saveStateToDatabase(context: Context) {
|
||||||
logD("Saving state to DB.")
|
logD("Saving state to DB.")
|
||||||
|
|
||||||
|
// Pack the entire state and save it to the database.
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val playbackState = packToPlaybackState()
|
|
||||||
val queueItems = packQueue()
|
|
||||||
|
|
||||||
val database = PlaybackStateDatabase.getInstance(context)
|
val database = PlaybackStateDatabase.getInstance(context)
|
||||||
database.writeState(playbackState)
|
|
||||||
database.writeQueue(queueItems)
|
database.writeState(packToPlaybackState())
|
||||||
|
database.writeQueue(packQueue())
|
||||||
|
|
||||||
|
this@PlaybackStateManager.logD(
|
||||||
|
"Save finished in ${System.currentTimeMillis() - start}ms"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val time = System.currentTimeMillis() - start
|
|
||||||
|
|
||||||
logD("Save finished in ${time}ms")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore the state from the database
|
* Restore the state from the database
|
||||||
* @param context [Context] required.
|
* @param context [Context] required.
|
||||||
*/
|
*/
|
||||||
suspend fun getStateFromDatabase(context: Context) {
|
suspend fun restoreFromDatabase(context: Context) {
|
||||||
logD("Getting state from DB.")
|
logD("Getting state from DB.")
|
||||||
|
|
||||||
val start: Long
|
val now: Long
|
||||||
|
|
||||||
val state: PlaybackState?
|
val state: PlaybackState?
|
||||||
|
|
||||||
|
// The coroutine call is locked at queueItems so that this function does not
|
||||||
|
// go ahead until EVERYTHING is read.
|
||||||
val queueItems = withContext(Dispatchers.IO) {
|
val queueItems = withContext(Dispatchers.IO) {
|
||||||
start = System.currentTimeMillis()
|
now = System.currentTimeMillis()
|
||||||
|
|
||||||
val database = PlaybackStateDatabase.getInstance(context)
|
val database = PlaybackStateDatabase.getInstance(context)
|
||||||
|
|
||||||
state = database.readState()
|
state = database.readState()
|
||||||
database.readQueue()
|
database.readQueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
val loadTime = System.currentTimeMillis() - start
|
// Get off the IO coroutine since it will cause LiveData updates to throw an exception
|
||||||
|
|
||||||
logD("Load finished in ${loadTime}ms")
|
|
||||||
|
|
||||||
state?.let {
|
state?.let {
|
||||||
logD("Valid playback state $it")
|
logD("Valid playback state $it")
|
||||||
|
@ -663,11 +643,9 @@ class PlaybackStateManager private constructor() {
|
||||||
doParentSanityCheck()
|
doParentSanityCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
val time = System.currentTimeMillis() - start
|
logD("Restore finished in ${System.currentTimeMillis() - now}ms")
|
||||||
|
|
||||||
logD("Restore finished in ${time}ms")
|
setRestored()
|
||||||
|
|
||||||
mIsRestored = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -693,8 +671,7 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unpack the state from a [PlaybackState]
|
* Unpack a [playbackState] into this instance.
|
||||||
* @param playbackState The state to unpack.
|
|
||||||
*/
|
*/
|
||||||
private fun unpackFromPlaybackState(playbackState: PlaybackState) {
|
private fun unpackFromPlaybackState(playbackState: PlaybackState) {
|
||||||
// Turn the simplified information from PlaybackState into values that can be used
|
// Turn the simplified information from PlaybackState into values that can be used
|
||||||
|
|
|
@ -14,6 +14,7 @@ import org.oxycblt.auxio.settings.SettingsManager
|
||||||
/**
|
/**
|
||||||
* Object that manages the AudioFocus state.
|
* Object that manages the AudioFocus state.
|
||||||
* Adapted from NewPipe (https://github.com/TeamNewPipe/NewPipe)
|
* Adapted from NewPipe (https://github.com/TeamNewPipe/NewPipe)
|
||||||
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class AudioReactor(
|
class AudioReactor(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
|
@ -10,6 +10,7 @@ import android.os.Build
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.media.app.NotificationCompat.MediaStyle
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.MainActivity
|
import org.oxycblt.auxio.MainActivity
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
@ -18,7 +19,6 @@ import org.oxycblt.auxio.music.Parent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.state.LoopMode
|
import org.oxycblt.auxio.playback.state.LoopMode
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import androidx.media.app.NotificationCompat as MediaNotificationCompat
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The unified notification for [PlaybackService]. This is not self-sufficient, updates have
|
* The unified notification for [PlaybackService]. This is not self-sufficient, updates have
|
||||||
|
@ -51,7 +51,7 @@ class PlaybackNotification private constructor(
|
||||||
addAction(buildAction(context, ACTION_EXIT, R.drawable.ic_exit))
|
addAction(buildAction(context, ACTION_EXIT, R.drawable.ic_exit))
|
||||||
|
|
||||||
setStyle(
|
setStyle(
|
||||||
MediaNotificationCompat.MediaStyle()
|
MediaStyle()
|
||||||
.setMediaSession(mediaToken)
|
.setMediaSession(mediaToken)
|
||||||
.setShowActionsInCompactView(1, 2, 3)
|
.setShowActionsInCompactView(1, 2, 3)
|
||||||
)
|
)
|
||||||
|
@ -186,7 +186,7 @@ class PlaybackNotification private constructor(
|
||||||
const val ACTION_EXIT = "ACTION_AUXIO_EXIT_" + BuildConfig.BUILD_TYPE
|
const val ACTION_EXIT = "ACTION_AUXIO_EXIT_" + BuildConfig.BUILD_TYPE
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a new instance of [PlaybackNotification].
|
* Build a new instance of [PlaybackNotification] from a [context] and [mediaSession]
|
||||||
*/
|
*/
|
||||||
fun from(context: Context, mediaSession: MediaSessionCompat): PlaybackNotification {
|
fun from(context: Context, mediaSession: MediaSessionCompat): PlaybackNotification {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
|
|
@ -49,7 +49,6 @@ import org.oxycblt.auxio.settings.SettingsManager
|
||||||
* - The single [SimpleExoPlayer] instance.
|
* - The single [SimpleExoPlayer] instance.
|
||||||
* - The [MediaSessionCompat]
|
* - The [MediaSessionCompat]
|
||||||
* - The Media Notification
|
* - The Media Notification
|
||||||
* - Audio Focus
|
|
||||||
* - Headset management
|
* - Headset management
|
||||||
*
|
*
|
||||||
* This service relies on [PlaybackStateManager.Callback] and [SettingsManager.Callback],
|
* This service relies on [PlaybackStateManager.Callback] and [SettingsManager.Callback],
|
||||||
|
@ -485,6 +484,9 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
||||||
private inner class SystemEventReceiver : BroadcastReceiver() {
|
private inner class SystemEventReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
|
|
||||||
|
// --- NOTIFICATION CASES ---
|
||||||
|
|
||||||
PlaybackNotification.ACTION_PLAY_PAUSE -> playbackManager.setPlaying(
|
PlaybackNotification.ACTION_PLAY_PAUSE -> playbackManager.setPlaying(
|
||||||
!playbackManager.isPlaying
|
!playbackManager.isPlaying
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,7 +11,6 @@ import org.oxycblt.auxio.music.Song
|
||||||
/**
|
/**
|
||||||
* An enum for the current sorting mode. Contains helper functions to sort lists based
|
* An enum for the current sorting mode. Contains helper functions to sort lists based
|
||||||
* off the given sorting mode.
|
* off the given sorting mode.
|
||||||
* TODO: Improve sorting by separating UP/DOWN from what should be sorted (Names, Tracks, etc)
|
|
||||||
* @property iconRes The icon for this [SortMode]
|
* @property iconRes The icon for this [SortMode]
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -9,8 +9,8 @@ import org.oxycblt.auxio.music.BaseModel
|
||||||
* A [RecyclerView.ViewHolder] that streamlines a lot of the common things across all viewholders.
|
* A [RecyclerView.ViewHolder] that streamlines a lot of the common things across all viewholders.
|
||||||
* @param T The datatype, inheriting [BaseModel] for this ViewHolder.
|
* @param T The datatype, inheriting [BaseModel] for this ViewHolder.
|
||||||
* @param binding Basic [ViewDataBinding] required to set up click listeners & sizing.
|
* @param binding Basic [ViewDataBinding] required to set up click listeners & sizing.
|
||||||
* @param doOnClick (Optional, defaults to null) Function that specifies what to do on a click. Null if nothing should be done.
|
* @param doOnClick (Optional) Function that calls on a click.
|
||||||
* @param doOnLongClick (Optional, defaults to null) Functions that specifics what to do on a long click. Null if nothing should be done.
|
* @param doOnLongClick (Optional) Functions that calls on a long-click.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
abstract class BaseViewHolder<T : BaseModel>(
|
abstract class BaseViewHolder<T : BaseModel>(
|
||||||
|
@ -38,8 +38,8 @@ abstract class BaseViewHolder<T : BaseModel>(
|
||||||
}
|
}
|
||||||
|
|
||||||
doOnLongClick?.let { onLongClick ->
|
doOnLongClick?.let { onLongClick ->
|
||||||
binding.root.setOnLongClickListener {
|
binding.root.setOnLongClickListener { view ->
|
||||||
onLongClick(binding.root, data)
|
onLongClick(view, data)
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,7 @@ class SearchFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.searchEditText.addTextChangedListener {
|
binding.searchEditText.addTextChangedListener {
|
||||||
|
// Run the search with the updated text as the query
|
||||||
searchModel.doSearch(it?.toString() ?: "", requireContext())
|
searchModel.doSearch(it?.toString() ?: "", requireContext())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,29 +145,29 @@ class SearchFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to an item, or play it, depending on what the given item is.
|
* Function that handles when an [item] is selected.
|
||||||
* @param baseModel The data the action should be done with
|
* Handles all datatypes that are selectable.
|
||||||
*/
|
*/
|
||||||
private fun onItemSelection(baseModel: BaseModel) {
|
private fun onItemSelection(item: BaseModel) {
|
||||||
if (baseModel is Song) {
|
if (item is Song) {
|
||||||
playbackModel.playSong(baseModel)
|
playbackModel.playSong(item)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get rid of the keyboard
|
// Get rid of the keyboard if we are navigating
|
||||||
requireView().rootView.clearFocus()
|
requireView().rootView.clearFocus()
|
||||||
|
|
||||||
if (!searchModel.isNavigating) {
|
if (!searchModel.isNavigating) {
|
||||||
searchModel.updateNavigationStatus(true)
|
searchModel.updateNavigationStatus(true)
|
||||||
|
|
||||||
logD("Navigating to the detail fragment for ${baseModel.name}")
|
logD("Navigating to the detail fragment for ${item.name}")
|
||||||
|
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
when (baseModel) {
|
when (item) {
|
||||||
is Genre -> SearchFragmentDirections.actionShowGenre(baseModel.id)
|
is Genre -> SearchFragmentDirections.actionShowGenre(item.id)
|
||||||
is Artist -> SearchFragmentDirections.actionShowArtist(baseModel.id)
|
is Artist -> SearchFragmentDirections.actionShowArtist(item.id)
|
||||||
is Album -> SearchFragmentDirections.actionShowAlbum(baseModel.id)
|
is Album -> SearchFragmentDirections.actionShowAlbum(item.id)
|
||||||
|
|
||||||
// If given model wasn't valid, then reset the navigation status
|
// If given model wasn't valid, then reset the navigation status
|
||||||
// and abort the navigation.
|
// and abort the navigation.
|
||||||
|
|
|
@ -20,15 +20,14 @@ import org.oxycblt.auxio.settings.SettingsManager
|
||||||
*/
|
*/
|
||||||
class SearchViewModel : ViewModel() {
|
class SearchViewModel : ViewModel() {
|
||||||
private val mSearchResults = MutableLiveData(listOf<BaseModel>())
|
private val mSearchResults = MutableLiveData(listOf<BaseModel>())
|
||||||
val searchResults: LiveData<List<BaseModel>> get() = mSearchResults
|
private var mIsNavigating = false
|
||||||
|
|
||||||
private var mFilterMode = DisplayMode.SHOW_ALL
|
private var mFilterMode = DisplayMode.SHOW_ALL
|
||||||
val filterMode: DisplayMode get() = mFilterMode
|
|
||||||
|
|
||||||
private var mLastQuery = ""
|
private var mLastQuery = ""
|
||||||
|
|
||||||
private var mIsNavigating = false
|
/** Current search results from the last [doSearch] call. */
|
||||||
|
val searchResults: LiveData<List<BaseModel>> get() = mSearchResults
|
||||||
val isNavigating: Boolean get() = mIsNavigating
|
val isNavigating: Boolean get() = mIsNavigating
|
||||||
|
val filterMode: DisplayMode get() = mFilterMode
|
||||||
|
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
@ -38,9 +37,8 @@ class SearchViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform a search of the music library. Will push results to [searchResults].
|
* Use [query] to perform a search of the music library.
|
||||||
* @param query The query to use
|
* Will push results to [searchResults].
|
||||||
* @param context [Context] required to create the headers
|
|
||||||
*/
|
*/
|
||||||
fun doSearch(query: String, context: Context) {
|
fun doSearch(query: String, context: Context) {
|
||||||
mLastQuery = query
|
mLastQuery = query
|
||||||
|
@ -86,6 +84,10 @@ class SearchViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the current filter mode with a menu [id].
|
||||||
|
* New value will be pushed to [filterMode].
|
||||||
|
*/
|
||||||
fun updateFilterModeWithId(@IdRes id: Int, context: Context) {
|
fun updateFilterModeWithId(@IdRes id: Int, context: Context) {
|
||||||
mFilterMode = DisplayMode.fromId(id)
|
mFilterMode = DisplayMode.fromId(id)
|
||||||
|
|
||||||
|
@ -94,6 +96,10 @@ class SearchViewModel : ViewModel() {
|
||||||
doSearch(mLastQuery, context)
|
doSearch(mLastQuery, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcut that will run a ignoreCase filter on a list and only return
|
||||||
|
* a value if the resulting list is empty.
|
||||||
|
*/
|
||||||
private fun List<BaseModel>.filterByOrNull(value: String): List<BaseModel>? {
|
private fun List<BaseModel>.filterByOrNull(value: String): List<BaseModel>? {
|
||||||
val filtered = filter { it.name.contains(value, ignoreCase = true) }
|
val filtered = filter { it.name.contains(value, ignoreCase = true) }
|
||||||
|
|
||||||
|
@ -101,10 +107,9 @@ class SearchViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the current navigation status
|
* Update the current navigation status to [isNavigating]
|
||||||
* @param value Whether LibraryFragment is navigating or not
|
|
||||||
*/
|
*/
|
||||||
fun updateNavigationStatus(value: Boolean) {
|
fun updateNavigationStatus(isNavigating: Boolean) {
|
||||||
mIsNavigating = value
|
mIsNavigating = isNavigating
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,9 @@ class SettingsListFragment : PreferenceFragmentCompat() {
|
||||||
setPreferencesFromResource(R.xml.prefs_main, rootKey)
|
setPreferencesFromResource(R.xml.prefs_main, rootKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively call [handlePreference] on a preference.
|
||||||
|
*/
|
||||||
private fun recursivelyHandleChildren(pref: Preference) {
|
private fun recursivelyHandleChildren(pref: Preference) {
|
||||||
if (pref is PreferenceCategory) {
|
if (pref is PreferenceCategory) {
|
||||||
// If this preference is a category of its own, handle its own children
|
// If this preference is a category of its own, handle its own children
|
||||||
|
@ -54,6 +57,9 @@ class SettingsListFragment : PreferenceFragmentCompat() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a preference, doing any specific actions on it.
|
||||||
|
*/
|
||||||
private fun handlePreference(pref: Preference) {
|
private fun handlePreference(pref: Preference) {
|
||||||
pref.apply {
|
pref.apply {
|
||||||
when (key) {
|
when (key) {
|
||||||
|
@ -134,6 +140,9 @@ class SettingsListFragment : PreferenceFragmentCompat() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the accent dialog to the user
|
||||||
|
*/
|
||||||
private fun showAccentDialog() {
|
private fun showAccentDialog() {
|
||||||
MaterialDialog(requireActivity()).show {
|
MaterialDialog(requireActivity()).show {
|
||||||
title(R.string.setting_accent)
|
title(R.string.setting_accent)
|
||||||
|
|
|
@ -23,15 +23,11 @@ class SettingsManager private constructor(context: Context) :
|
||||||
|
|
||||||
// --- VALUES ---
|
// --- VALUES ---
|
||||||
|
|
||||||
/**
|
/** The current theme */
|
||||||
* The current theme.
|
|
||||||
*/
|
|
||||||
val theme: Int
|
val theme: Int
|
||||||
get() = sharedPrefs.getString(Keys.KEY_THEME, EntryValues.THEME_AUTO)!!.toThemeInt()
|
get() = sharedPrefs.getString(Keys.KEY_THEME, EntryValues.THEME_AUTO)!!.toThemeInt()
|
||||||
|
|
||||||
/**
|
/** The current accent. */
|
||||||
* The current accent.
|
|
||||||
*/
|
|
||||||
var accent: Accent
|
var accent: Accent
|
||||||
get() {
|
get() {
|
||||||
// Accent is stored as an index [to be efficient], so retrieve it when done.
|
// Accent is stored as an index [to be efficient], so retrieve it when done.
|
||||||
|
@ -47,9 +43,7 @@ class SettingsManager private constructor(context: Context) :
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Whether to colorize the notification */
|
||||||
* Whether to colorize the notification
|
|
||||||
*/
|
|
||||||
val colorizeNotif: Boolean
|
val colorizeNotif: Boolean
|
||||||
get() = sharedPrefs.getBoolean(Keys.KEY_COLORIZE_NOTIFICATION, true)
|
get() = sharedPrefs.getBoolean(Keys.KEY_COLORIZE_NOTIFICATION, true)
|
||||||
|
|
||||||
|
@ -60,9 +54,7 @@ class SettingsManager private constructor(context: Context) :
|
||||||
val useAltNotifAction: Boolean
|
val useAltNotifAction: Boolean
|
||||||
get() = sharedPrefs.getBoolean(Keys.KEY_USE_ALT_NOTIFICATION_ACTION, false)
|
get() = sharedPrefs.getBoolean(Keys.KEY_USE_ALT_NOTIFICATION_ACTION, false)
|
||||||
|
|
||||||
/**
|
/** What to display on the library. */
|
||||||
* What to display on the library.
|
|
||||||
*/
|
|
||||||
val libraryDisplayMode: DisplayMode
|
val libraryDisplayMode: DisplayMode
|
||||||
get() = DisplayMode.valueOfOrFallback(
|
get() = DisplayMode.valueOfOrFallback(
|
||||||
sharedPrefs.getString(
|
sharedPrefs.getString(
|
||||||
|
@ -78,27 +70,19 @@ class SettingsManager private constructor(context: Context) :
|
||||||
val showCovers: Boolean
|
val showCovers: Boolean
|
||||||
get() = sharedPrefs.getBoolean(Keys.KEY_SHOW_COVERS, true)
|
get() = sharedPrefs.getBoolean(Keys.KEY_SHOW_COVERS, true)
|
||||||
|
|
||||||
/**
|
/** Whether to ignore MediaStore covers */
|
||||||
* Whether to ignore MediaStore covers
|
|
||||||
*/
|
|
||||||
val useQualityCovers: Boolean
|
val useQualityCovers: Boolean
|
||||||
get() = sharedPrefs.getBoolean(Keys.KEY_QUALITY_COVERS, false)
|
get() = sharedPrefs.getBoolean(Keys.KEY_QUALITY_COVERS, false)
|
||||||
|
|
||||||
/**
|
/** Whether to do Audio focus. */
|
||||||
* Whether to do Audio focus.
|
|
||||||
*/
|
|
||||||
val doAudioFocus: Boolean
|
val doAudioFocus: Boolean
|
||||||
get() = sharedPrefs.getBoolean(Keys.KEY_AUDIO_FOCUS, true)
|
get() = sharedPrefs.getBoolean(Keys.KEY_AUDIO_FOCUS, true)
|
||||||
|
|
||||||
/**
|
/** Whether to resume/stop playback when a headset is connected/disconnected. */
|
||||||
* Whether to resume/stop playback when a headset is connected/disconnected.
|
|
||||||
*/
|
|
||||||
val doPlugMgt: Boolean
|
val doPlugMgt: Boolean
|
||||||
get() = sharedPrefs.getBoolean(Keys.KEY_PLUG_MANAGEMENT, true)
|
get() = sharedPrefs.getBoolean(Keys.KEY_PLUG_MANAGEMENT, true)
|
||||||
|
|
||||||
/**
|
/** What queue to create when a song is selected (ex. From All Songs or Search) */
|
||||||
* What queue to create when a song is selected (ex. From All Songs or Search)
|
|
||||||
*/
|
|
||||||
val songPlaybackMode: PlaybackMode
|
val songPlaybackMode: PlaybackMode
|
||||||
get() = PlaybackMode.valueOfOrFallback(
|
get() = PlaybackMode.valueOfOrFallback(
|
||||||
sharedPrefs.getString(
|
sharedPrefs.getString(
|
||||||
|
@ -107,28 +91,20 @@ class SettingsManager private constructor(context: Context) :
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/** What to do at the end of a playlist. */
|
||||||
* What to do at the end of a playlist.
|
|
||||||
*/
|
|
||||||
val doAtEnd: String
|
val doAtEnd: String
|
||||||
get() = sharedPrefs.getString(Keys.KEY_AT_END, EntryValues.AT_END_LOOP_PAUSE)
|
get() = sharedPrefs.getString(Keys.KEY_AT_END, EntryValues.AT_END_LOOP_PAUSE)
|
||||||
?: EntryValues.AT_END_LOOP_PAUSE
|
?: EntryValues.AT_END_LOOP_PAUSE
|
||||||
|
|
||||||
/**
|
/** Whether shuffle should stay on when a new song is selected. */
|
||||||
* Whether shuffle should stay on when a new song is selected.
|
|
||||||
*/
|
|
||||||
val keepShuffle: Boolean
|
val keepShuffle: Boolean
|
||||||
get() = sharedPrefs.getBoolean(Keys.KEY_KEEP_SHUFFLE, true)
|
get() = sharedPrefs.getBoolean(Keys.KEY_KEEP_SHUFFLE, true)
|
||||||
|
|
||||||
/**
|
/** Whether to rewind when the back button is pressed. */
|
||||||
* Whether to rewind when the back button is pressed.
|
|
||||||
*/
|
|
||||||
val rewindWithPrev: Boolean
|
val rewindWithPrev: Boolean
|
||||||
get() = sharedPrefs.getBoolean(Keys.KEY_PREV_REWIND, true)
|
get() = sharedPrefs.getBoolean(Keys.KEY_PREV_REWIND, true)
|
||||||
|
|
||||||
/**
|
/** The current [SortMode] of the library. */
|
||||||
* The current [SortMode] of the library.
|
|
||||||
*/
|
|
||||||
var librarySortMode: SortMode
|
var librarySortMode: SortMode
|
||||||
get() = SortMode.fromInt(
|
get() = SortMode.fromInt(
|
||||||
sharedPrefs.getInt(
|
sharedPrefs.getInt(
|
||||||
|
@ -143,9 +119,7 @@ class SettingsManager private constructor(context: Context) :
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The current filter mode of the search tab */
|
||||||
* The current filter mode of the search tab
|
|
||||||
*/
|
|
||||||
var searchFilterMode: DisplayMode
|
var searchFilterMode: DisplayMode
|
||||||
get() = DisplayMode.valueOfOrFallback(
|
get() = DisplayMode.valueOfOrFallback(
|
||||||
sharedPrefs.getString(
|
sharedPrefs.getString(
|
||||||
|
|
|
@ -46,7 +46,8 @@ class AboutDialog : BottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Go through the process of opening one of the about links in a browser.
|
* Go through the process of opening a [link] in a browser. Only supports the links
|
||||||
|
* in [AboutDialog.Companion.LINKS].
|
||||||
*/
|
*/
|
||||||
private fun openLinkInBrowser(link: String) {
|
private fun openLinkInBrowser(link: String) {
|
||||||
check(link in LINKS) { "Invalid link." }
|
check(link in LINKS) { "Invalid link." }
|
||||||
|
@ -83,9 +84,9 @@ class AboutDialog : BottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logE("Browser intent launching failed [Probably android's fault]")
|
logE("Browser intent launching failed [Probably android's fault]")
|
||||||
e.printStackTrace()
|
logE(e.stackTraceToString())
|
||||||
|
|
||||||
// Sometimes people have """""Browsers""""" on their phone according to android,
|
// Sometimes people have """Browsers""" on their phone according to android,
|
||||||
// but they actually don't so here's a fallback for that.
|
// but they actually don't so here's a fallback for that.
|
||||||
getString(R.string.error_no_browser).createToast(requireContext())
|
getString(R.string.error_no_browser).createToast(requireContext())
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import org.oxycblt.auxio.recycler.viewholders.SongViewHolder
|
||||||
* @param data List of [Song]s to be shown
|
* @param data List of [Song]s to be shown
|
||||||
* @param doOnClick What to do on a click action
|
* @param doOnClick What to do on a click action
|
||||||
* @param doOnLongClick What to do on a long click action
|
* @param doOnLongClick What to do on a long click action
|
||||||
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class SongsAdapter(
|
class SongsAdapter(
|
||||||
private val data: List<Song>,
|
private val data: List<Song>,
|
||||||
|
|
|
@ -18,23 +18,25 @@ import org.oxycblt.auxio.logD
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.Accent
|
import org.oxycblt.auxio.ui.Accent
|
||||||
|
import org.oxycblt.auxio.ui.canScroll
|
||||||
import org.oxycblt.auxio.ui.getSpans
|
import org.oxycblt.auxio.ui.getSpans
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Fragment] that shows a list of all songs on the device. Contains options to search/shuffle
|
* A [Fragment] that shows a list of all songs on the device.
|
||||||
* them.
|
* Contains options to search/shuffle them.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class SongsFragment : Fragment() {
|
class SongsFragment : Fragment() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
private val musicStore = MusicStore.getInstance()
|
||||||
|
|
||||||
// Lazy init the text size so that it doesn't have to be calculated every time.
|
// Lazy init the text size so that it doesn't have to be calculated every time.
|
||||||
private val indicatorTextSize: Float by lazy {
|
private val indicatorTextSize: Float by lazy {
|
||||||
TypedValue.applyDimension(
|
TypedValue.applyDimension(
|
||||||
TypedValue.COMPLEX_UNIT_SP, 14F,
|
TypedValue.COMPLEX_UNIT_SP, 14F,
|
||||||
requireContext().resources.displayMetrics
|
resources.displayMetrics
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,8 +46,6 @@ class SongsFragment : Fragment() {
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
val binding = FragmentSongsBinding.inflate(inflater)
|
val binding = FragmentSongsBinding.inflate(inflater)
|
||||||
|
|
||||||
val musicStore = MusicStore.getInstance()
|
|
||||||
val songAdapter = SongsAdapter(musicStore.songs, playbackModel::playSong) { view, data ->
|
val songAdapter = SongsAdapter(musicStore.songs, playbackModel::playSong) { view, data ->
|
||||||
newMenu(view, data)
|
newMenu(view, data)
|
||||||
}
|
}
|
||||||
|
@ -54,13 +54,12 @@ class SongsFragment : Fragment() {
|
||||||
|
|
||||||
binding.songToolbar.apply {
|
binding.songToolbar.apply {
|
||||||
setOnMenuItemClickListener {
|
setOnMenuItemClickListener {
|
||||||
when (it.itemId) {
|
if (it.itemId == R.id.action_shuffle) {
|
||||||
R.id.action_shuffle -> {
|
|
||||||
playbackModel.shuffleAll()
|
playbackModel.shuffleAll()
|
||||||
}
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +75,7 @@ class SongsFragment : Fragment() {
|
||||||
|
|
||||||
post {
|
post {
|
||||||
// Disable fast scrolling if there is nothing to scroll
|
// Disable fast scrolling if there is nothing to scroll
|
||||||
if (computeVerticalScrollRange() < height) {
|
if (!canScroll()) {
|
||||||
binding.songFastScroll.visibility = View.GONE
|
binding.songFastScroll.visibility = View.GONE
|
||||||
binding.songFastScrollThumb.visibility = View.GONE
|
binding.songFastScrollThumb.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
@ -101,8 +100,6 @@ class SongsFragment : Fragment() {
|
||||||
* @param binding Binding required
|
* @param binding Binding required
|
||||||
*/
|
*/
|
||||||
private fun setupFastScroller(binding: FragmentSongsBinding) {
|
private fun setupFastScroller(binding: FragmentSongsBinding) {
|
||||||
val musicStore = MusicStore.getInstance()
|
|
||||||
|
|
||||||
binding.songFastScroll.apply {
|
binding.songFastScroll.apply {
|
||||||
var concatInterval = -1
|
var concatInterval = -1
|
||||||
|
|
||||||
|
@ -172,25 +169,27 @@ class SongsFragment : Fragment() {
|
||||||
|
|
||||||
useDefaultScroller = false
|
useDefaultScroller = false
|
||||||
|
|
||||||
|
addIndicatorCallback { pos ->
|
||||||
|
binding.songRecycler.apply {
|
||||||
|
(layoutManager as LinearLayoutManager).scrollToPositionWithOffset(pos, 0)
|
||||||
|
|
||||||
|
stopScroll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.songFastScrollThumb.setupWithFastScroller(binding.songFastScroll)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun FastScrollerView.addIndicatorCallback(callback: (pos: Int) -> Unit) {
|
||||||
itemIndicatorSelectedCallbacks.add(
|
itemIndicatorSelectedCallbacks.add(
|
||||||
object : FastScrollerView.ItemIndicatorSelectedCallback {
|
object : FastScrollerView.ItemIndicatorSelectedCallback {
|
||||||
override fun onItemIndicatorSelected(
|
override fun onItemIndicatorSelected(
|
||||||
indicator: FastScrollItemIndicator,
|
indicator: FastScrollItemIndicator,
|
||||||
indicatorCenterY: Int,
|
indicatorCenterY: Int,
|
||||||
itemPosition: Int
|
itemPosition: Int
|
||||||
) {
|
) = callback(itemPosition)
|
||||||
binding.songRecycler.apply {
|
|
||||||
(layoutManager as LinearLayoutManager).scrollToPositionWithOffset(
|
|
||||||
itemPosition, 0
|
|
||||||
)
|
|
||||||
|
|
||||||
stopScroll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.songFastScrollThumb.setupWithFastScroller(binding.songFastScroll)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ val ACCENTS = arrayOf(
|
||||||
* @property color The color resource for this accent
|
* @property color The color resource for this accent
|
||||||
* @property theme The theme resource for this accent
|
* @property theme The theme resource for this accent
|
||||||
* @property name The name of this accent
|
* @property name The name of this accent
|
||||||
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
data class Accent(@ColorRes val color: Int, @StyleRes val theme: Int, @StringRes val name: Int) {
|
data class Accent(@ColorRes val color: Int, @StyleRes val theme: Int, @StringRes val name: Int) {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -34,7 +34,8 @@ fun Fragment.newMenu(anchor: View, data: BaseModel, flag: Int = ActionMenu.FLAG_
|
||||||
* @param anchor [View] This should be centered around
|
* @param anchor [View] This should be centered around
|
||||||
* @param data [BaseModel] this menu corresponds to
|
* @param data [BaseModel] this menu corresponds to
|
||||||
* @param flag Any extra flags to accompany the data. See [FLAG_NONE], [FLAG_IN_ALBUM], [FLAG_IN_ARTIST], [FLAG_IN_GENRE] for more details.
|
* @param flag Any extra flags to accompany the data. See [FLAG_NONE], [FLAG_IN_ALBUM], [FLAG_IN_ARTIST], [FLAG_IN_GENRE] for more details.
|
||||||
* @throws IllegalArgumentException When there is no menu for this specific datatype/flag
|
* @throws IllegalStateException When there is no menu for this specific datatype/flag
|
||||||
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class ActionMenu(
|
class ActionMenu(
|
||||||
activity: AppCompatActivity,
|
activity: AppCompatActivity,
|
||||||
|
|
|
@ -167,6 +167,11 @@ fun RecyclerView.getSpans(): Int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether a recyclerview can scroll.
|
||||||
|
*/
|
||||||
|
fun RecyclerView.canScroll() = computeVerticalScrollRange() > height
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if we are in the "Irregular" landscape mode (e.g landscape, but nav bar is on the sides)
|
* Check if we are in the "Irregular" landscape mode (e.g landscape, but nav bar is on the sides)
|
||||||
* Used to disable most of edge-to-edge if that's the case, as I cant get it to work on this mode.
|
* Used to disable most of edge-to-edge if that's the case, as I cant get it to work on this mode.
|
||||||
|
|
|
@ -26,6 +26,7 @@ fun <T : ViewDataBinding> Fragment.memberBinding(
|
||||||
/**
|
/**
|
||||||
* The delegate for the [memberBinding] shortcut function.
|
* The delegate for the [memberBinding] shortcut function.
|
||||||
* Adapted from KAHelpers (https://github.com/FunkyMuse/KAHelpers/tree/master/viewbinding)
|
* Adapted from KAHelpers (https://github.com/FunkyMuse/KAHelpers/tree/master/viewbinding)
|
||||||
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class MemberBinder<T : ViewDataBinding>(
|
class MemberBinder<T : ViewDataBinding>(
|
||||||
private val fragment: Fragment,
|
private val fragment: Fragment,
|
||||||
|
|
|
@ -17,6 +17,7 @@ import java.lang.reflect.Field
|
||||||
*
|
*
|
||||||
* Adapted from this StackOverflow answer:
|
* Adapted from this StackOverflow answer:
|
||||||
* https://stackoverflow.com/a/35087229
|
* https://stackoverflow.com/a/35087229
|
||||||
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class SlideLinearLayout @JvmOverloads constructor(
|
class SlideLinearLayout @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
Loading…
Reference in a new issue