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.
|
||||
if (intent != null) {
|
||||
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) {
|
||||
// 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 ->
|
||||
playbackModel.playWithUri(fileUri, this)
|
||||
|
@ -100,6 +100,6 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
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
|
||||
* quality covers or not.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class AlbumArtFetcher(private val context: Context) : Fetcher<Album> {
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
|
|
@ -19,7 +19,7 @@ import org.oxycblt.auxio.settings.SettingsManager
|
|||
// --- BINDING ADAPTERS ---
|
||||
|
||||
/**
|
||||
* Bind the album art for a [Song].
|
||||
* Bind the album art for a [song].
|
||||
*/
|
||||
@BindingAdapter("albumArt")
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
fun ImageView.bindGenreImage(genre: Genre) {
|
||||
|
|
|
@ -2,7 +2,6 @@ package org.oxycblt.auxio.database
|
|||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import android.os.Looper
|
||||
|
@ -35,9 +34,7 @@ class PlaybackStateDatabase(context: Context) :
|
|||
// --- DATABASE CONSTRUCTION FUNCTIONS ---
|
||||
|
||||
/**
|
||||
* Create a table
|
||||
* @param database DB to create the tables on
|
||||
* @param tableName The name of the table to create.
|
||||
* Create a table for this database.
|
||||
*/
|
||||
private fun createTable(database: SQLiteDatabase, tableName: String) {
|
||||
val command = StringBuilder()
|
||||
|
@ -106,15 +103,17 @@ class PlaybackStateDatabase(context: Context) :
|
|||
|
||||
val stateData = ContentValues(9)
|
||||
|
||||
stateData.put(PlaybackState.COLUMN_ID, state.id)
|
||||
stateData.put(PlaybackState.COLUMN_SONG_NAME, state.songName)
|
||||
stateData.put(PlaybackState.COLUMN_POSITION, state.position)
|
||||
stateData.put(PlaybackState.COLUMN_PARENT_NAME, state.parentName)
|
||||
stateData.put(PlaybackState.COLUMN_INDEX, state.index)
|
||||
stateData.put(PlaybackState.COLUMN_MODE, state.mode)
|
||||
stateData.put(PlaybackState.COLUMN_IS_SHUFFLING, state.isShuffling)
|
||||
stateData.put(PlaybackState.COLUMN_LOOP_MODE, state.loopMode)
|
||||
stateData.put(PlaybackState.COLUMN_IN_USER_QUEUE, state.inUserQueue)
|
||||
stateData.apply {
|
||||
put(PlaybackState.COLUMN_ID, state.id)
|
||||
put(PlaybackState.COLUMN_SONG_NAME, state.songName)
|
||||
put(PlaybackState.COLUMN_POSITION, state.position)
|
||||
put(PlaybackState.COLUMN_PARENT_NAME, state.parentName)
|
||||
put(PlaybackState.COLUMN_INDEX, state.index)
|
||||
put(PlaybackState.COLUMN_MODE, state.mode)
|
||||
put(PlaybackState.COLUMN_IS_SHUFFLING, state.isShuffling)
|
||||
put(PlaybackState.COLUMN_LOOP_MODE, state.loopMode)
|
||||
put(PlaybackState.COLUMN_IN_USER_QUEUE, state.inUserQueue)
|
||||
}
|
||||
|
||||
database.insert(TABLE_NAME_STATE, null, stateData)
|
||||
database.setTransactionSuccessful()
|
||||
|
@ -135,10 +134,9 @@ class PlaybackStateDatabase(context: Context) :
|
|||
val database = writableDatabase
|
||||
|
||||
var state: PlaybackState? = null
|
||||
val stateCursor: Cursor
|
||||
|
||||
try {
|
||||
stateCursor = database.query(
|
||||
val stateCursor = database.query(
|
||||
TABLE_NAME_STATE,
|
||||
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.
|
||||
* @param queue The list of [QueueItem]s to be written.
|
||||
* Write a list of [queueItems] to the database, clearing the previous queue present.
|
||||
*/
|
||||
fun writeQueue(queue: List<QueueItem>) {
|
||||
fun writeQueue(queueItems: List<QueueItem>) {
|
||||
assertBackgroundThread()
|
||||
|
||||
val database = readableDatabase
|
||||
|
@ -202,21 +199,23 @@ class PlaybackStateDatabase(context: Context) :
|
|||
var position = 0
|
||||
|
||||
// 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()
|
||||
var i = position
|
||||
|
||||
try {
|
||||
while (i < queue.size) {
|
||||
val item = queue[i]
|
||||
while (i < queueItems.size) {
|
||||
val item = queueItems[i]
|
||||
val itemData = ContentValues(4)
|
||||
|
||||
i++
|
||||
|
||||
itemData.put(QueueItem.COLUMN_ID, item.id)
|
||||
itemData.put(QueueItem.COLUMN_SONG_NAME, item.songName)
|
||||
itemData.put(QueueItem.COLUMN_ALBUM_NAME, item.albumName)
|
||||
itemData.put(QueueItem.COLUMN_IS_USER_QUEUE, item.isUserQueue)
|
||||
itemData.apply {
|
||||
put(QueueItem.COLUMN_ID, item.id)
|
||||
put(QueueItem.COLUMN_SONG_NAME, item.songName)
|
||||
put(QueueItem.COLUMN_ALBUM_NAME, item.albumName)
|
||||
put(QueueItem.COLUMN_IS_USER_QUEUE, item.isUserQueue)
|
||||
}
|
||||
|
||||
database.insert(TABLE_NAME_QUEUE, null, itemData)
|
||||
}
|
||||
|
@ -242,12 +241,10 @@ class PlaybackStateDatabase(context: Context) :
|
|||
assertBackgroundThread()
|
||||
|
||||
val database = readableDatabase
|
||||
|
||||
val queueItems = mutableListOf<QueueItem>()
|
||||
val queueCursor: Cursor
|
||||
|
||||
try {
|
||||
queueCursor = database.query(
|
||||
val queueCursor = database.query(
|
||||
TABLE_NAME_QUEUE, 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.recycler.CenterSmoothScroller
|
||||
import org.oxycblt.auxio.ui.ActionMenu
|
||||
import org.oxycblt.auxio.ui.canScroll
|
||||
import org.oxycblt.auxio.ui.createToast
|
||||
import org.oxycblt.auxio.ui.newMenu
|
||||
|
||||
|
@ -83,7 +84,6 @@ class AlbumDetailFragment : DetailFragment() {
|
|||
}
|
||||
|
||||
detailModel.navToItem.observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
when (it) {
|
||||
// Songs should be scrolled to if the album matches, or a new detail
|
||||
// fragment should be launched otherwise.
|
||||
|
@ -122,7 +122,6 @@ class AlbumDetailFragment : DetailFragment() {
|
|||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- PLAYBACKVIEWMODEL SETUP ---
|
||||
|
||||
|
@ -148,6 +147,9 @@ class AlbumDetailFragment : DetailFragment() {
|
|||
return binding.root
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to an song using its [id].
|
||||
*/
|
||||
private fun scrollToItem(id: Long) {
|
||||
// Calculate where the item for the currently played song is
|
||||
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
|
||||
// correctly center the playing item, so make sure that the Toolbar is lifted in
|
||||
// that case.
|
||||
if (binding.detailRecycler.computeVerticalScrollRange() > binding.detailRecycler.height) {
|
||||
binding.detailAppbar.isLifted = true
|
||||
}
|
||||
binding.detailAppbar.isLifted = binding.detailRecycler.canScroll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,6 +51,8 @@ abstract class DetailFragment : Fragment() {
|
|||
|
||||
/**
|
||||
* 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(
|
||||
@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
|
||||
* [SortMode] they are currently on.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class DetailViewModel : ViewModel() {
|
||||
private var mIsNavigating = false
|
||||
val isNavigating: Boolean get() = mIsNavigating
|
||||
private val mCurrentGenre = MutableLiveData<Genre?>()
|
||||
private val mCurrentArtist = MutableLiveData<Artist?>()
|
||||
private val mCurrentAlbum = MutableLiveData<Album?>()
|
||||
|
||||
private val mGenreSortMode = MutableLiveData(SortMode.ALPHA_DOWN)
|
||||
val genreSortMode: LiveData<SortMode> get() = mGenreSortMode
|
||||
|
||||
private val mArtistSortMode = MutableLiveData(SortMode.NUMERIC_DOWN)
|
||||
val artistSortMode: LiveData<SortMode> get() = mArtistSortMode
|
||||
|
||||
private val mAlbumSortMode = MutableLiveData(SortMode.NUMERIC_DOWN)
|
||||
val albumSortMode: LiveData<SortMode> get() = mAlbumSortMode
|
||||
|
||||
// Current music models being shown
|
||||
private val mCurrentGenre = MutableLiveData<Genre?>()
|
||||
private val mNavToItem = MutableLiveData<BaseModel?>()
|
||||
private var mIsNavigating = false
|
||||
|
||||
val currentGenre: LiveData<Genre?> get() = mCurrentGenre
|
||||
|
||||
private val mCurrentArtist = MutableLiveData<Artist?>()
|
||||
val currentArtist: LiveData<Artist?> get() = mCurrentArtist
|
||||
|
||||
private val mCurrentAlbum = MutableLiveData<Album?>()
|
||||
val currentAlbum: LiveData<Album?> get() = mCurrentAlbum
|
||||
|
||||
// Primary navigation flag.
|
||||
private val mNavToItem = MutableLiveData<BaseModel?>()
|
||||
val genreSortMode: LiveData<SortMode> get() = mGenreSortMode
|
||||
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
|
||||
|
||||
/**
|
||||
* Update the current navigation status
|
||||
* @param value Whether the current [DetailFragment] is navigating or not.
|
||||
*/
|
||||
fun updateNavigationStatus(value: Boolean) {
|
||||
mIsNavigating = value
|
||||
fun updateGenre(genre: Genre) {
|
||||
mCurrentGenre.value = genre
|
||||
}
|
||||
|
||||
fun updateArtist(artist: Artist) {
|
||||
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() {
|
||||
mAlbumSortMode.value = when (mAlbumSortMode.value) {
|
||||
|
@ -86,25 +89,24 @@ class DetailViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
fun updateGenre(genre: Genre) {
|
||||
mCurrentGenre.value = genre
|
||||
}
|
||||
|
||||
fun updateArtist(artist: Artist) {
|
||||
mCurrentArtist.value = artist
|
||||
}
|
||||
|
||||
fun updateAlbum(album: Album) {
|
||||
mCurrentAlbum.value = album
|
||||
}
|
||||
|
||||
/** Navigate to an item, whether a song/album/artist */
|
||||
/**
|
||||
* Navigate to an item, whether a song/album/artist
|
||||
*/
|
||||
fun navToItem(item: BaseModel) {
|
||||
mNavToItem.value = item
|
||||
}
|
||||
|
||||
/** Mark that the navigation process is done. */
|
||||
/**
|
||||
* Mark that the navigation process is done.
|
||||
*/
|
||||
fun doneWithNavToItem() {
|
||||
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) {
|
||||
if (it != null) {
|
||||
when (it) {
|
||||
// All items will launch new detail fragments.
|
||||
is Artist -> findNavController().navigate(
|
||||
|
@ -88,7 +87,6 @@ class GenreDetailFragment : DetailFragment() {
|
|||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- PLAYBACKVIEWMODEL SETUP ---
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.oxycblt.auxio.ui.setTextColorResource
|
|||
|
||||
/**
|
||||
* An adapter for displaying the details and [Song]s of an [Album]
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class AlbumDetailAdapter(
|
||||
private val detailModel: DetailViewModel,
|
||||
|
@ -75,8 +76,8 @@ class AlbumDetailAdapter(
|
|||
}
|
||||
|
||||
/**
|
||||
* Update the current song that this adapter should be watching for to highlight.
|
||||
* @param song The [Song] to highlight if found, null to clear any highlighted ViewHolders
|
||||
* Update the [song] that this adapter should highlight
|
||||
* @param recycler The recyclerview the highlighting should act on.
|
||||
*/
|
||||
fun highlightSong(song: Song?, recycler: RecyclerView) {
|
||||
// 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.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class ArtistDetailAdapter(
|
||||
private val detailModel: DetailViewModel,
|
||||
|
@ -76,8 +77,8 @@ class ArtistDetailAdapter(
|
|||
}
|
||||
|
||||
/**
|
||||
* Update the current album that this adapter should be watching for to highlight.
|
||||
* @param album The [Album] to highlight if found, null to clear any highlighted ViewHolders
|
||||
* Update the current [album] that this adapter should highlight
|
||||
* @param recycler The recyclerview the highlighting should act on.
|
||||
*/
|
||||
fun setCurrentAlbum(album: Album?, recycler: RecyclerView) {
|
||||
// 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.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class GenreDetailAdapter(
|
||||
private val detailModel: DetailViewModel,
|
||||
|
@ -76,8 +77,8 @@ class GenreDetailAdapter(
|
|||
}
|
||||
|
||||
/**
|
||||
* Update the current song that this adapter should be watching for to highlight.
|
||||
* @param song The [Song] to highlight if found, null to clear any highlighted ViewHolders
|
||||
* Update the [song] that this adapter should highlight
|
||||
* @param recycler The recyclerview the highlighting should act on.
|
||||
*/
|
||||
fun highlightSong(song: Song?, recycler: RecyclerView) {
|
||||
// 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
|
||||
* @param newData The new data to be used
|
||||
* Update the data with [newData]. [notifyDataSetChanged] will be called.
|
||||
*/
|
||||
fun updateData(newData: List<Parent>) {
|
||||
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
|
||||
* search functionality.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class LibraryFragment : Fragment() {
|
||||
private val libraryModel: LibraryViewModel by activityViewModels()
|
||||
|
@ -36,7 +37,7 @@ class LibraryFragment : Fragment() {
|
|||
): View {
|
||||
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 ---
|
||||
|
||||
|
@ -78,9 +79,9 @@ class LibraryFragment : Fragment() {
|
|||
libraryModel.updateNavigationStatus(false)
|
||||
|
||||
if (it is Parent) {
|
||||
onItemSelection(it)
|
||||
navToDetail(it)
|
||||
} else if (it is Song) {
|
||||
onItemSelection(it.album)
|
||||
navToDetail(it.album)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,10 +104,9 @@ class LibraryFragment : Fragment() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Navigate to a parent UI
|
||||
* @param parent The parent that should be navigated with
|
||||
* Navigate to the detail UI for a [parent].
|
||||
*/
|
||||
private fun onItemSelection(parent: Parent) {
|
||||
private fun navToDetail(parent: Parent) {
|
||||
requireView().rootView.clearFocus()
|
||||
|
||||
if (!libraryModel.isNavigating) {
|
||||
|
|
|
@ -20,14 +20,14 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
|
|||
private val mLibraryData = MutableLiveData(listOf<Parent>())
|
||||
val libraryData: LiveData<List<Parent>> get() = mLibraryData
|
||||
|
||||
private var mIsNavigating = false
|
||||
val isNavigating: Boolean get() = mIsNavigating
|
||||
|
||||
private var mSortMode = SortMode.ALPHA_DOWN
|
||||
val sortMode: SortMode get() = mSortMode
|
||||
|
||||
private var mDisplayMode = DisplayMode.SHOW_ARTISTS
|
||||
|
||||
private var mIsNavigating = false
|
||||
val isNavigating: Boolean get() = mIsNavigating
|
||||
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
|
||||
|
@ -42,8 +42,7 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
|
|||
}
|
||||
|
||||
/**
|
||||
* Update the current [SortMode] with a menu id.
|
||||
* @param itemId The id of the menu item selected.
|
||||
* Update the current [SortMode] using an menu [itemId].
|
||||
*/
|
||||
fun updateSortMode(@IdRes itemId: Int) {
|
||||
val mode = when (itemId) {
|
||||
|
@ -64,28 +63,11 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
|
|||
|
||||
/**
|
||||
* Update the current navigation status
|
||||
* @param value Whether LibraryFragment is navigating or not
|
||||
*/
|
||||
fun updateNavigationStatus(value: Boolean) {
|
||||
mIsNavigating = value
|
||||
fun updateNavigationStatus(isNavigating: Boolean) {
|
||||
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]
|
||||
*/
|
||||
|
@ -100,4 +82,18 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback {
|
|||
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.music.MusicStore
|
||||
|
||||
/**
|
||||
* Fragment that handles what to display during the loading process.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class LoadingFragment : Fragment() {
|
||||
private val loadingModel: LoadingViewModel by viewModels {
|
||||
LoadingViewModel.Factory(requireActivity().application)
|
||||
|
@ -85,6 +89,9 @@ class LoadingFragment : Fragment() {
|
|||
|
||||
// --- PERMISSIONS ---
|
||||
|
||||
/**
|
||||
* Check if Auxio has the permissions to load music
|
||||
*/
|
||||
private fun hasNoPermissions(): Boolean {
|
||||
val needRationale = shouldShowRequestPermissionRationale(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
|
@ -107,16 +114,24 @@ class LoadingFragment : Fragment() {
|
|||
|
||||
// --- UI DISPLAY ---
|
||||
|
||||
/**
|
||||
* Hide all error elements and return to the loading view
|
||||
*/
|
||||
private fun showLoading(binding: FragmentLoadingBinding) {
|
||||
binding.apply {
|
||||
loadingCircle.visibility = View.VISIBLE
|
||||
loadingErrorIcon.visibility = View.GONE
|
||||
loadingErrorText.visibility = View.GONE
|
||||
loadingRetryButton.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) {
|
||||
binding.loadingCircle.visibility = View.GONE
|
||||
binding.loadingErrorIcon.visibility = View.VISIBLE
|
||||
|
|
|
@ -9,17 +9,25 @@ import androidx.lifecycle.viewModelScope
|
|||
import kotlinx.coroutines.launch
|
||||
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() {
|
||||
private val mResponse = MutableLiveData<MusicStore.Response?>(null)
|
||||
val response: LiveData<MusicStore.Response?> = mResponse
|
||||
|
||||
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
|
||||
|
||||
private var isBusy = false
|
||||
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
|
||||
/**
|
||||
* Begin the music loading process. The response is pushed to [response]
|
||||
*/
|
||||
fun load() {
|
||||
// Dont start a new load if the last one hasnt finished
|
||||
if (isBusy) return
|
||||
|
@ -33,14 +41,23 @@ class LoadingViewModel(private val app: Application) : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the grant prompt.
|
||||
*/
|
||||
fun grant() {
|
||||
mDoGrant.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark that the grant prompt is now shown.
|
||||
*/
|
||||
fun doneWithGrant() {
|
||||
mDoGrant.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify this viewmodel that there are no permissions
|
||||
*/
|
||||
fun notifyNoPermissions() {
|
||||
mResponse.value = MusicStore.Response.NO_PERMS
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import android.net.Uri
|
|||
* The base data object for all music.
|
||||
* @property id The ID that is assigned to this object
|
||||
* @property name The name of this object (Such as a song title)
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
sealed class BaseModel {
|
||||
abstract val id: Long
|
||||
|
@ -34,7 +33,6 @@ sealed class Parent : BaseModel() {
|
|||
* @property genre The Song's [Genre]
|
||||
* @property seconds The Song's duration in seconds
|
||||
* @property formattedDuration The Song's duration as a duration string.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
data class Song(
|
||||
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 songs The Album's child [Song]s.
|
||||
* @property totalDuration The combined duration of all of the album's child songs, formatted.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
data class Album(
|
||||
override val id: Long = -1,
|
||||
|
@ -108,7 +105,6 @@ data class Album(
|
|||
* @property albums The list of all [Album]s in this artist
|
||||
* @property genre The most prominent genre for this artist
|
||||
* @property songs The list of all [Song]s in this artist
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
data class Artist(
|
||||
override val id: Long = -1,
|
||||
|
@ -134,14 +130,13 @@ data class Artist(
|
|||
* The data object for a genre. Inherits [Parent]
|
||||
* @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.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
data class Genre(
|
||||
override val id: Long = -1,
|
||||
override val name: String,
|
||||
) : Parent() {
|
||||
val resolvedName: String by lazy {
|
||||
if (name.contains(Regex("[0123456789)]"))) {
|
||||
if (name.contains(Regex("([1-9])"))) {
|
||||
name.toNamedGenre() ?: name
|
||||
} else {
|
||||
name
|
||||
|
@ -162,7 +157,6 @@ data class Genre(
|
|||
/**
|
||||
* 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.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
data class Header(
|
||||
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
|
||||
private set
|
||||
|
||||
/**
|
||||
* Load/Sort the entire music library.
|
||||
* ***THIS SHOULD ONLY BE RAN FROM AN IO THREAD.***
|
||||
* Load/Sort the entire music library. Should always be ran on a coroutine.
|
||||
* @param app [Application] required to load the music.
|
||||
*/
|
||||
suspend fun load(app: Application): Response {
|
||||
|
@ -68,8 +68,6 @@ class MusicStore private constructor() {
|
|||
mArtists = linker.artists.toList()
|
||||
mGenres = linker.genres.toList()
|
||||
|
||||
loaded = true
|
||||
|
||||
this@MusicStore.logD(
|
||||
"Music load completed successfully in ${System.currentTimeMillis() - start}ms."
|
||||
)
|
||||
|
@ -80,12 +78,15 @@ class MusicStore private constructor() {
|
|||
return@withContext Response.FAILED
|
||||
}
|
||||
|
||||
loaded = true
|
||||
|
||||
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? {
|
||||
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 {
|
||||
NO_MUSIC, NO_PERMS, FAILED, SUCCESS
|
||||
}
|
||||
|
|
|
@ -60,25 +60,17 @@ fun String.toNamedGenre(): String? {
|
|||
}
|
||||
|
||||
/**
|
||||
* Convert a song id to its URI
|
||||
* @return The [Uri] for this song/
|
||||
* Convert an id to its corresponding URI
|
||||
*/
|
||||
fun Long.toURI(): Uri {
|
||||
return ContentUris.withAppendedId(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||
this
|
||||
)
|
||||
return ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URI for an album's cover art.
|
||||
* @return The [Uri] for the album's cover art/
|
||||
* Get the URI for an album's cover art, corresponds to MediaStore.
|
||||
*/
|
||||
fun Long.toAlbumArtURI(): Uri {
|
||||
return ContentUris.withAppendedId(
|
||||
Uri.parse("content://media/external/audio/albumart"),
|
||||
this
|
||||
)
|
||||
return ContentUris.withAppendedId(Uri.parse("content://media/external/audio/albumart"), this)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -10,7 +10,9 @@ import org.oxycblt.auxio.music.Genre
|
|||
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(
|
||||
private val context: Context,
|
||||
|
@ -21,6 +23,10 @@ class MusicLinker(
|
|||
private val resolver = context.contentResolver
|
||||
val artists = mutableListOf<Artist>()
|
||||
|
||||
/**
|
||||
* Begin the linking process.
|
||||
* Modified models are pushed to [songs], [albums], [artists], and [genres]
|
||||
*/
|
||||
fun link() {
|
||||
linkAlbums()
|
||||
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
|
||||
* Artists are constructed in [MusicLinker], as they are only really containers for [Album]s
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class MusicLoader(private val app: Application) {
|
||||
var genres = mutableListOf<Genre>()
|
||||
|
@ -25,6 +26,9 @@ class MusicLoader(private val app: Application) {
|
|||
|
||||
private val resolver = app.contentResolver
|
||||
|
||||
/**
|
||||
* Begin the loading process. Resulting models are pushed to [genres], [albums], and [songs].
|
||||
*/
|
||||
fun loadMusic() {
|
||||
loadGenres()
|
||||
loadAlbums()
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.oxycblt.auxio.ui.toAnimDrawable
|
|||
|
||||
/**
|
||||
* Custom [AppCompatImageButton] that handles the animated play/pause icons.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class PlayPauseButton @JvmOverloads constructor(
|
||||
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) {
|
||||
if (isPlaying) {
|
||||
if (animate) {
|
||||
|
|
|
@ -121,7 +121,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
playbackManager.playSong(song, mode)
|
||||
}
|
||||
|
||||
/** Play an album.*/
|
||||
/**
|
||||
* Play an [album].
|
||||
* @param shuffled Whether to shuffle the new queue
|
||||
*/
|
||||
fun playAlbum(album: Album, shuffled: Boolean) {
|
||||
if (album.songs.isEmpty()) {
|
||||
logE("Album is empty, Not playing.")
|
||||
|
@ -132,7 +135,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
playbackManager.playParent(album, shuffled)
|
||||
}
|
||||
|
||||
/** Play an Artist */
|
||||
/**
|
||||
* Play an [artist].
|
||||
* @param shuffled Whether to shuffle the new queue
|
||||
*/
|
||||
fun playArtist(artist: Artist, shuffled: Boolean) {
|
||||
if (artist.songs.isEmpty()) {
|
||||
logE("Artist is empty, Not playing.")
|
||||
|
@ -143,7 +149,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
playbackManager.playParent(artist, shuffled)
|
||||
}
|
||||
|
||||
/** Play a genre. */
|
||||
/**
|
||||
* Play a [genre].
|
||||
* @param shuffled Whether to shuffle the new queue
|
||||
*/
|
||||
fun playGenre(genre: Genre, shuffled: Boolean) {
|
||||
if (genre.songs.isEmpty()) {
|
||||
logE("Genre is empty, Not playing.")
|
||||
|
@ -154,7 +163,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
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) {
|
||||
// Check if everything is already running to run the URI play
|
||||
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) {
|
||||
viewModelScope.launch {
|
||||
musicStore.getSongForUri(uri, context.contentResolver)?.let { song ->
|
||||
|
@ -173,14 +188,18 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
}
|
||||
}
|
||||
|
||||
/** Shuffle all songs */
|
||||
/**
|
||||
* Shuffle all songs
|
||||
*/
|
||||
fun shuffleAll() {
|
||||
playbackManager.shuffleAll()
|
||||
}
|
||||
|
||||
// --- POSITION FUNCTIONS ---
|
||||
|
||||
/** Update the position and push it to [PlaybackStateManager] */
|
||||
/**
|
||||
* Update the position and push it to [PlaybackStateManager]
|
||||
*/
|
||||
fun setPosition(progress: Int) {
|
||||
playbackManager.seekTo((progress * 1000).toLong())
|
||||
}
|
||||
|
@ -196,22 +215,25 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
|
||||
// --- QUEUE FUNCTIONS ---
|
||||
|
||||
/** Skip to the next song. */
|
||||
/**
|
||||
* Skip to the next song.
|
||||
*/
|
||||
fun skipNext() {
|
||||
playbackManager.next()
|
||||
}
|
||||
|
||||
/** Skip to the previous song */
|
||||
/**
|
||||
* Skip to the previous song.
|
||||
*/
|
||||
fun skipPrev() {
|
||||
playbackManager.prev()
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a queue OR user queue item, given a QueueAdapter index.
|
||||
* @param adapterIndex The [QueueAdapter] index to remove
|
||||
* @param queueAdapter The [QueueAdapter] itself to push changes to when successful.
|
||||
* Remove an item at [adapterIndex], being a non-header data index.
|
||||
* @param queueAdapter [QueueAdapter] instance to push changes to when successful.
|
||||
*/
|
||||
fun removeQueueAdapterItem(adapterIndex: Int, queueAdapter: QueueAdapter) {
|
||||
fun removeQueueDataItem(adapterIndex: Int, queueAdapter: QueueAdapter) {
|
||||
var index = adapterIndex.dec()
|
||||
|
||||
// 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.
|
||||
* @param adapterFrom The [QueueAdapter] index that needs to be moved
|
||||
* @param adapterTo The destination [QueueAdapter] index.
|
||||
* @param queueAdapter the [QueueAdapter] to push changes to when successful.
|
||||
* Move a queue OR user queue item from [adapterFrom] to [adapterTo], as long as both
|
||||
* indices are non-header data indices.
|
||||
* @param queueAdapter [QueueAdapter] instance to push changes to when successful.
|
||||
*/
|
||||
fun moveQueueAdapterItems(
|
||||
fun moveQueueDataItems(
|
||||
adapterFrom: Int,
|
||||
adapterTo: Int,
|
||||
queueAdapter: QueueAdapter
|
||||
|
@ -283,38 +304,50 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
return true
|
||||
}
|
||||
|
||||
/** Add a [Song] to the user queue.*/
|
||||
/**
|
||||
* Add a [Song] to the user queue.
|
||||
*/
|
||||
fun addToUserQueue(song: Song) {
|
||||
playbackManager.addToUserQueue(song)
|
||||
}
|
||||
|
||||
/** Add an [Album] to the user queue */
|
||||
/**
|
||||
* Add an [Album] to the user queue
|
||||
*/
|
||||
fun addToUserQueue(album: Album) {
|
||||
val songs = SortMode.NUMERIC_DOWN.getSortedSongList(album.songs)
|
||||
|
||||
playbackManager.addToUserQueue(songs)
|
||||
}
|
||||
|
||||
/** Clear the user queue entirely */
|
||||
/**
|
||||
* Clear the user queue entirely
|
||||
*/
|
||||
fun clearUserQueue() {
|
||||
playbackManager.clearUserQueue()
|
||||
}
|
||||
|
||||
// --- STATUS FUNCTIONS ---
|
||||
|
||||
/** Flip the playing status, e.g from playing to paused */
|
||||
/**
|
||||
* Flip the playing status, e.g from playing to paused
|
||||
*/
|
||||
fun invertPlayingStatus() {
|
||||
enableAnimation()
|
||||
|
||||
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() {
|
||||
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() {
|
||||
playbackManager.setLoopMode(playbackManager.loopMode.increment())
|
||||
}
|
||||
|
@ -322,8 +355,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
// --- SAVE/RESTORE FUNCTIONS ---
|
||||
|
||||
/**
|
||||
* Force save the current [PlaybackStateManager] state to the database. Called by SettingsListFragment.
|
||||
* @param context [Context] required.
|
||||
* Force save the current [PlaybackStateManager] state to the database.
|
||||
* Called by SettingsListFragment.
|
||||
*/
|
||||
fun savePlaybackState(context: Context) {
|
||||
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) {
|
||||
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.
|
||||
mIntentUri = null
|
||||
} else if (!playbackManager.isRestored) {
|
||||
// Otherwise just restore
|
||||
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() {
|
||||
logD("Attempting to restore playback state.")
|
||||
|
||||
|
@ -371,17 +410,23 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
|
||||
// --- OTHER FUNCTIONS ---
|
||||
|
||||
/** Set whether the seeking indicator should be highlighted */
|
||||
fun setSeekingStatus(value: Boolean) {
|
||||
mIsSeeking.value = value
|
||||
/**
|
||||
* Set whether the seeking indicator should be highlighted
|
||||
*/
|
||||
fun setSeekingStatus(isSeeking: Boolean) {
|
||||
mIsSeeking.value = isSeeking
|
||||
}
|
||||
|
||||
/** Enable animation on CompactPlaybackFragment */
|
||||
/**
|
||||
* Enable animation on CompactPlaybackFragment
|
||||
*/
|
||||
fun enableAnimation() {
|
||||
mCanAnimate = true
|
||||
}
|
||||
|
||||
/** Disable animation on CompactPlaybackFragment */
|
||||
/**
|
||||
* Disable animation on CompactPlaybackFragment
|
||||
*/
|
||||
fun disableAnimation() {
|
||||
mCanAnimate = false
|
||||
}
|
||||
|
|
|
@ -38,14 +38,16 @@ class QueueAdapter(
|
|||
override fun getItemViewType(position: Int): Int {
|
||||
val item = data[position]
|
||||
|
||||
return if (item is Header)
|
||||
if (item.isAction)
|
||||
return if (item is Header) {
|
||||
if (item.isAction) {
|
||||
USER_QUEUE_HEADER_ITEM_TYPE
|
||||
else
|
||||
} else {
|
||||
HeaderViewHolder.ITEM_TYPE
|
||||
else
|
||||
}
|
||||
} else {
|
||||
QUEUE_SONG_ITEM_TYPE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
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>) {
|
||||
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) {
|
||||
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) {
|
||||
data.removeAt(adapterIndex)
|
||||
|
||||
/*
|
||||
* Check for two things:
|
||||
* If the data from the next queue is now entirely empty [Signified by a header at the end]
|
||||
* Or if the data from the last queue is now entirely empty [Signified by there being
|
||||
* 2 headers with no items in between]
|
||||
* If so, remove the header and the removed item in a range. Otherwise just remove the item.
|
||||
* If the data from the next queue is now entirely empty [Signified by a header at the
|
||||
* end, remove the next queue header as notify as such.
|
||||
*
|
||||
* If the user queue is empty [Signified by there being two headers at the beginning with
|
||||
* 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) {
|
||||
val lastIndex = data.lastIndex
|
||||
|
|
|
@ -11,7 +11,6 @@ import kotlin.math.sign
|
|||
/**
|
||||
* The Drag callback used by the queue recyclerview. Delivers updates to [PlaybackViewModel]
|
||||
* and [QueueAdapter] simultaneously.
|
||||
* @param playbackModel The [PlaybackViewModel] required to dispatch updates to.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouchHelper.Callback() {
|
||||
|
@ -58,7 +57,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
|||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
return playbackModel.moveQueueAdapterItems(
|
||||
return playbackModel.moveQueueDataItems(
|
||||
viewHolder.adapterPosition,
|
||||
target.adapterPosition,
|
||||
queueAdapter
|
||||
|
@ -66,7 +65,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
|||
}
|
||||
|
||||
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
|
||||
* edit them as well.
|
||||
*
|
||||
* Instantiation is done by the navigation component, **do not instantiate this fragment manually.**
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class QueueFragment : Fragment() {
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.oxycblt.auxio.playback.state
|
|||
|
||||
/**
|
||||
* Enum that determines the playback repeat mode.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
enum class LoopMode {
|
||||
NONE, ONCE, INFINITE;
|
||||
|
@ -35,7 +36,7 @@ enum class LoopMode {
|
|||
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.
|
||||
*/
|
||||
fun fromInt(constant: Int): LoopMode? {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
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 {
|
||||
/** Construct the queue from the genre's songs */
|
||||
IN_GENRE,
|
||||
|
@ -31,7 +34,7 @@ enum class PlaybackMode {
|
|||
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.
|
||||
*/
|
||||
fun fromInt(constant: Int): PlaybackMode? {
|
||||
|
|
|
@ -144,8 +144,7 @@ class PlaybackStateManager private constructor() {
|
|||
// --- PLAYING FUNCTIONS ---
|
||||
|
||||
/**
|
||||
* Play a song.
|
||||
* @param song The song to be played
|
||||
* Play a [song].
|
||||
* @param mode The [PlaybackMode] to construct the queue off of.
|
||||
*/
|
||||
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.
|
||||
* @param parent The model to use
|
||||
* @param shuffled Whether to shuffle the queue or not
|
||||
* Play a [parent], such as an artist or album.
|
||||
* @param shuffled Whether the queue is shuffled or not
|
||||
*/
|
||||
fun playParent(parent: Parent, shuffled: Boolean) {
|
||||
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!!!***
|
||||
* @param song The song to play
|
||||
* Shuffle all songs.
|
||||
*/
|
||||
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) {
|
||||
mIsInUserQueue = false
|
||||
|
@ -326,8 +335,7 @@ class PlaybackStateManager private constructor() {
|
|||
// --- QUEUE EDITING FUNCTIONS ---
|
||||
|
||||
/**
|
||||
* Remove a queue item at a QUEUE index. Will log an error if the index is out of bounds
|
||||
* @param index The index at which the item should be removed.
|
||||
* Remove a queue item at [index]. Will ignore invalid indexes.
|
||||
*/
|
||||
fun removeQueueItem(index: Int): Boolean {
|
||||
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
|
||||
* is out of bounds.
|
||||
* @param from The starting item's index
|
||||
* @param to The destination index.
|
||||
* Move a queue item at [from] to a position at [to]. Will ignore invalid indexes.
|
||||
*/
|
||||
fun moveQueueItems(from: Int, to: Int): Boolean {
|
||||
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.
|
||||
* @param song The song to add
|
||||
* Add a [song] to the user queue.
|
||||
*/
|
||||
fun addToUserQueue(song: Song) {
|
||||
mUserQueue.add(song)
|
||||
|
@ -377,8 +381,7 @@ class PlaybackStateManager private constructor() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Add a list of songs to the user queue.
|
||||
* @param songs The songs to add.
|
||||
* Add a list of [songs] to the user queue.
|
||||
*/
|
||||
fun addToUserQueue(songs: List<Song>) {
|
||||
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.
|
||||
* @param index The index at which the item should be removed.
|
||||
* Remove a USER queue item at [index]. Will ignore invalid indexes.
|
||||
*/
|
||||
fun removeUserQueueItem(index: Int) {
|
||||
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
|
||||
* is out of bounds.
|
||||
* @param from The starting item's index
|
||||
* @param to The destination index.
|
||||
* Move a USER queue item at [from] to a position at [to]. Will ignore invalid indexes.
|
||||
*/
|
||||
fun moveUserQueueItems(from: Int, to: Int) {
|
||||
if (from > mUserQueue.size || from < 0 || to > mUserQueue.size || to < 0) {
|
||||
|
@ -449,24 +448,11 @@ class PlaybackStateManager private constructor() {
|
|||
// --- SHUFFLE FUNCTIONS ---
|
||||
|
||||
/**
|
||||
* Shuffle all songs.
|
||||
*/
|
||||
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.
|
||||
* Set whether this instance is [shuffled]. Updates the queue accordingly
|
||||
* @param keepSong Whether the current song should be kept as the queue is shuffled/unshuffled
|
||||
*/
|
||||
fun setShuffling(shuffling: Boolean, keepSong: Boolean) {
|
||||
mIsShuffling = shuffling
|
||||
fun setShuffling(shuffled: Boolean, keepSong: Boolean) {
|
||||
mIsShuffling = shuffled
|
||||
|
||||
if (mIsShuffling) {
|
||||
genShuffle(keepSong, mIsInUserQueue)
|
||||
|
@ -525,8 +511,7 @@ class PlaybackStateManager private constructor() {
|
|||
// --- STATE FUNCTIONS ---
|
||||
|
||||
/**
|
||||
* Set the current playing status
|
||||
* @param playing Whether the playback should be playing or paused.
|
||||
* Set whether this instance is currently [playing].
|
||||
*/
|
||||
fun setPlaying(playing: Boolean) {
|
||||
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.
|
||||
* @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.
|
||||
* @param position The position to seek to in millis.
|
||||
* @see setPosition
|
||||
*/
|
||||
fun seekTo(position: Long) {
|
||||
mPosition = position
|
||||
|
@ -573,15 +557,14 @@ class PlaybackStateManager private constructor() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set the [LoopMode]
|
||||
* @param mode The [LoopMode] to be used
|
||||
* Set the [LoopMode] to [mode].
|
||||
*/
|
||||
fun setLoopMode(mode: LoopMode) {
|
||||
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.
|
||||
*/
|
||||
fun clearLoopMode() {
|
||||
|
@ -614,45 +597,42 @@ class PlaybackStateManager private constructor() {
|
|||
suspend fun saveStateToDatabase(context: Context) {
|
||||
logD("Saving state to DB.")
|
||||
|
||||
// Pack the entire state and save it to the database.
|
||||
withContext(Dispatchers.IO) {
|
||||
val start = System.currentTimeMillis()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val playbackState = packToPlaybackState()
|
||||
val queueItems = packQueue()
|
||||
|
||||
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
|
||||
* @param context [Context] required.
|
||||
*/
|
||||
suspend fun getStateFromDatabase(context: Context) {
|
||||
suspend fun restoreFromDatabase(context: Context) {
|
||||
logD("Getting state from DB.")
|
||||
|
||||
val start: Long
|
||||
|
||||
val now: Long
|
||||
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) {
|
||||
start = System.currentTimeMillis()
|
||||
now = System.currentTimeMillis()
|
||||
|
||||
val database = PlaybackStateDatabase.getInstance(context)
|
||||
|
||||
state = database.readState()
|
||||
database.readQueue()
|
||||
}
|
||||
|
||||
val loadTime = System.currentTimeMillis() - start
|
||||
|
||||
logD("Load finished in ${loadTime}ms")
|
||||
// Get off the IO coroutine since it will cause LiveData updates to throw an exception
|
||||
|
||||
state?.let {
|
||||
logD("Valid playback state $it")
|
||||
|
@ -663,11 +643,9 @@ class PlaybackStateManager private constructor() {
|
|||
doParentSanityCheck()
|
||||
}
|
||||
|
||||
val time = System.currentTimeMillis() - start
|
||||
logD("Restore finished in ${System.currentTimeMillis() - now}ms")
|
||||
|
||||
logD("Restore finished in ${time}ms")
|
||||
|
||||
mIsRestored = true
|
||||
setRestored()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -693,8 +671,7 @@ class PlaybackStateManager private constructor() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Unpack the state from a [PlaybackState]
|
||||
* @param playbackState The state to unpack.
|
||||
* Unpack a [playbackState] into this instance.
|
||||
*/
|
||||
private fun unpackFromPlaybackState(playbackState: PlaybackState) {
|
||||
// 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.
|
||||
* Adapted from NewPipe (https://github.com/TeamNewPipe/NewPipe)
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class AudioReactor(
|
||||
context: Context,
|
||||
|
|
|
@ -10,6 +10,7 @@ import android.os.Build
|
|||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.media.app.NotificationCompat.MediaStyle
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.MainActivity
|
||||
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.playback.state.LoopMode
|
||||
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
|
||||
|
@ -51,7 +51,7 @@ class PlaybackNotification private constructor(
|
|||
addAction(buildAction(context, ACTION_EXIT, R.drawable.ic_exit))
|
||||
|
||||
setStyle(
|
||||
MediaNotificationCompat.MediaStyle()
|
||||
MediaStyle()
|
||||
.setMediaSession(mediaToken)
|
||||
.setShowActionsInCompactView(1, 2, 3)
|
||||
)
|
||||
|
@ -186,7 +186,7 @@ class PlaybackNotification private constructor(
|
|||
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 {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
|
|
|
@ -49,7 +49,6 @@ import org.oxycblt.auxio.settings.SettingsManager
|
|||
* - The single [SimpleExoPlayer] instance.
|
||||
* - The [MediaSessionCompat]
|
||||
* - The Media Notification
|
||||
* - Audio Focus
|
||||
* - Headset management
|
||||
*
|
||||
* 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() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
|
||||
// --- NOTIFICATION CASES ---
|
||||
|
||||
PlaybackNotification.ACTION_PLAY_PAUSE -> playbackManager.setPlaying(
|
||||
!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
|
||||
* 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]
|
||||
* @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.
|
||||
* @param T The datatype, inheriting [BaseModel] for this ViewHolder.
|
||||
* @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 doOnLongClick (Optional, defaults to null) Functions that specifics what to do on a long click. Null if nothing should be done.
|
||||
* @param doOnClick (Optional) Function that calls on a click.
|
||||
* @param doOnLongClick (Optional) Functions that calls on a long-click.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
abstract class BaseViewHolder<T : BaseModel>(
|
||||
|
@ -38,8 +38,8 @@ abstract class BaseViewHolder<T : BaseModel>(
|
|||
}
|
||||
|
||||
doOnLongClick?.let { onLongClick ->
|
||||
binding.root.setOnLongClickListener {
|
||||
onLongClick(binding.root, data)
|
||||
binding.root.setOnLongClickListener { view ->
|
||||
onLongClick(view, data)
|
||||
|
||||
true
|
||||
}
|
||||
|
|
|
@ -76,6 +76,7 @@ class SearchFragment : Fragment() {
|
|||
}
|
||||
|
||||
binding.searchEditText.addTextChangedListener {
|
||||
// Run the search with the updated text as the query
|
||||
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.
|
||||
* @param baseModel The data the action should be done with
|
||||
* Function that handles when an [item] is selected.
|
||||
* Handles all datatypes that are selectable.
|
||||
*/
|
||||
private fun onItemSelection(baseModel: BaseModel) {
|
||||
if (baseModel is Song) {
|
||||
playbackModel.playSong(baseModel)
|
||||
private fun onItemSelection(item: BaseModel) {
|
||||
if (item is Song) {
|
||||
playbackModel.playSong(item)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Get rid of the keyboard
|
||||
// Get rid of the keyboard if we are navigating
|
||||
requireView().rootView.clearFocus()
|
||||
|
||||
if (!searchModel.isNavigating) {
|
||||
searchModel.updateNavigationStatus(true)
|
||||
|
||||
logD("Navigating to the detail fragment for ${baseModel.name}")
|
||||
logD("Navigating to the detail fragment for ${item.name}")
|
||||
|
||||
findNavController().navigate(
|
||||
when (baseModel) {
|
||||
is Genre -> SearchFragmentDirections.actionShowGenre(baseModel.id)
|
||||
is Artist -> SearchFragmentDirections.actionShowArtist(baseModel.id)
|
||||
is Album -> SearchFragmentDirections.actionShowAlbum(baseModel.id)
|
||||
when (item) {
|
||||
is Genre -> SearchFragmentDirections.actionShowGenre(item.id)
|
||||
is Artist -> SearchFragmentDirections.actionShowArtist(item.id)
|
||||
is Album -> SearchFragmentDirections.actionShowAlbum(item.id)
|
||||
|
||||
// If given model wasn't valid, then reset the navigation status
|
||||
// and abort the navigation.
|
||||
|
|
|
@ -20,15 +20,14 @@ import org.oxycblt.auxio.settings.SettingsManager
|
|||
*/
|
||||
class SearchViewModel : ViewModel() {
|
||||
private val mSearchResults = MutableLiveData(listOf<BaseModel>())
|
||||
val searchResults: LiveData<List<BaseModel>> get() = mSearchResults
|
||||
|
||||
private var mIsNavigating = false
|
||||
private var mFilterMode = DisplayMode.SHOW_ALL
|
||||
val filterMode: DisplayMode get() = mFilterMode
|
||||
|
||||
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 filterMode: DisplayMode get() = mFilterMode
|
||||
|
||||
private val musicStore = MusicStore.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].
|
||||
* @param query The query to use
|
||||
* @param context [Context] required to create the headers
|
||||
* Use [query] to perform a search of the music library.
|
||||
* Will push results to [searchResults].
|
||||
*/
|
||||
fun doSearch(query: String, context: Context) {
|
||||
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) {
|
||||
mFilterMode = DisplayMode.fromId(id)
|
||||
|
||||
|
@ -94,6 +96,10 @@ class SearchViewModel : ViewModel() {
|
|||
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>? {
|
||||
val filtered = filter { it.name.contains(value, ignoreCase = true) }
|
||||
|
||||
|
@ -101,10 +107,9 @@ class SearchViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Update the current navigation status
|
||||
* @param value Whether LibraryFragment is navigating or not
|
||||
* Update the current navigation status to [isNavigating]
|
||||
*/
|
||||
fun updateNavigationStatus(value: Boolean) {
|
||||
mIsNavigating = value
|
||||
fun updateNavigationStatus(isNavigating: Boolean) {
|
||||
mIsNavigating = isNavigating
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,6 +45,9 @@ class SettingsListFragment : PreferenceFragmentCompat() {
|
|||
setPreferencesFromResource(R.xml.prefs_main, rootKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively call [handlePreference] on a preference.
|
||||
*/
|
||||
private fun recursivelyHandleChildren(pref: Preference) {
|
||||
if (pref is PreferenceCategory) {
|
||||
// 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) {
|
||||
pref.apply {
|
||||
when (key) {
|
||||
|
@ -134,6 +140,9 @@ class SettingsListFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the accent dialog to the user
|
||||
*/
|
||||
private fun showAccentDialog() {
|
||||
MaterialDialog(requireActivity()).show {
|
||||
title(R.string.setting_accent)
|
||||
|
|
|
@ -23,15 +23,11 @@ class SettingsManager private constructor(context: Context) :
|
|||
|
||||
// --- VALUES ---
|
||||
|
||||
/**
|
||||
* The current theme.
|
||||
*/
|
||||
/** The current theme */
|
||||
val theme: Int
|
||||
get() = sharedPrefs.getString(Keys.KEY_THEME, EntryValues.THEME_AUTO)!!.toThemeInt()
|
||||
|
||||
/**
|
||||
* The current accent.
|
||||
*/
|
||||
/** The current accent. */
|
||||
var accent: Accent
|
||||
get() {
|
||||
// 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()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to colorize the notification
|
||||
*/
|
||||
/** Whether to colorize the notification */
|
||||
val colorizeNotif: Boolean
|
||||
get() = sharedPrefs.getBoolean(Keys.KEY_COLORIZE_NOTIFICATION, true)
|
||||
|
||||
|
@ -60,9 +54,7 @@ class SettingsManager private constructor(context: Context) :
|
|||
val useAltNotifAction: Boolean
|
||||
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
|
||||
get() = DisplayMode.valueOfOrFallback(
|
||||
sharedPrefs.getString(
|
||||
|
@ -78,27 +70,19 @@ class SettingsManager private constructor(context: Context) :
|
|||
val showCovers: Boolean
|
||||
get() = sharedPrefs.getBoolean(Keys.KEY_SHOW_COVERS, true)
|
||||
|
||||
/**
|
||||
* Whether to ignore MediaStore covers
|
||||
*/
|
||||
/** Whether to ignore MediaStore covers */
|
||||
val useQualityCovers: Boolean
|
||||
get() = sharedPrefs.getBoolean(Keys.KEY_QUALITY_COVERS, false)
|
||||
|
||||
/**
|
||||
* Whether to do Audio focus.
|
||||
*/
|
||||
/** Whether to do Audio focus. */
|
||||
val doAudioFocus: Boolean
|
||||
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
|
||||
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
|
||||
get() = PlaybackMode.valueOfOrFallback(
|
||||
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
|
||||
get() = sharedPrefs.getString(Keys.KEY_AT_END, 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
|
||||
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
|
||||
get() = sharedPrefs.getBoolean(Keys.KEY_PREV_REWIND, true)
|
||||
|
||||
/**
|
||||
* The current [SortMode] of the library.
|
||||
*/
|
||||
/** The current [SortMode] of the library. */
|
||||
var librarySortMode: SortMode
|
||||
get() = SortMode.fromInt(
|
||||
sharedPrefs.getInt(
|
||||
|
@ -143,9 +119,7 @@ class SettingsManager private constructor(context: Context) :
|
|||
.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* The current filter mode of the search tab
|
||||
*/
|
||||
/** The current filter mode of the search tab */
|
||||
var searchFilterMode: DisplayMode
|
||||
get() = DisplayMode.valueOfOrFallback(
|
||||
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) {
|
||||
check(link in LINKS) { "Invalid link." }
|
||||
|
@ -83,9 +84,9 @@ class AboutDialog : BottomSheetDialogFragment() {
|
|||
}
|
||||
} catch (e: Exception) {
|
||||
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.
|
||||
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 doOnClick What to do on a click action
|
||||
* @param doOnLongClick What to do on a long click action
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class SongsAdapter(
|
||||
private val data: List<Song>,
|
||||
|
|
|
@ -18,23 +18,25 @@ import org.oxycblt.auxio.logD
|
|||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.Accent
|
||||
import org.oxycblt.auxio.ui.canScroll
|
||||
import org.oxycblt.auxio.ui.getSpans
|
||||
import org.oxycblt.auxio.ui.newMenu
|
||||
import kotlin.math.ceil
|
||||
|
||||
/**
|
||||
* A [Fragment] that shows a list of all songs on the device. Contains options to search/shuffle
|
||||
* them.
|
||||
* A [Fragment] that shows a list of all songs on the device.
|
||||
* Contains options to search/shuffle them.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class SongsFragment : Fragment() {
|
||||
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.
|
||||
private val indicatorTextSize: Float by lazy {
|
||||
TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_SP, 14F,
|
||||
requireContext().resources.displayMetrics
|
||||
resources.displayMetrics
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -44,8 +46,6 @@ class SongsFragment : Fragment() {
|
|||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val binding = FragmentSongsBinding.inflate(inflater)
|
||||
|
||||
val musicStore = MusicStore.getInstance()
|
||||
val songAdapter = SongsAdapter(musicStore.songs, playbackModel::playSong) { view, data ->
|
||||
newMenu(view, data)
|
||||
}
|
||||
|
@ -54,13 +54,12 @@ class SongsFragment : Fragment() {
|
|||
|
||||
binding.songToolbar.apply {
|
||||
setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.action_shuffle -> {
|
||||
if (it.itemId == R.id.action_shuffle) {
|
||||
playbackModel.shuffleAll()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
true
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,7 +75,7 @@ class SongsFragment : Fragment() {
|
|||
|
||||
post {
|
||||
// Disable fast scrolling if there is nothing to scroll
|
||||
if (computeVerticalScrollRange() < height) {
|
||||
if (!canScroll()) {
|
||||
binding.songFastScroll.visibility = View.GONE
|
||||
binding.songFastScrollThumb.visibility = View.GONE
|
||||
}
|
||||
|
@ -101,8 +100,6 @@ class SongsFragment : Fragment() {
|
|||
* @param binding Binding required
|
||||
*/
|
||||
private fun setupFastScroller(binding: FragmentSongsBinding) {
|
||||
val musicStore = MusicStore.getInstance()
|
||||
|
||||
binding.songFastScroll.apply {
|
||||
var concatInterval = -1
|
||||
|
||||
|
@ -172,25 +169,27 @@ class SongsFragment : Fragment() {
|
|||
|
||||
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(
|
||||
object : FastScrollerView.ItemIndicatorSelectedCallback {
|
||||
override fun onItemIndicatorSelected(
|
||||
indicator: FastScrollItemIndicator,
|
||||
indicatorCenterY: Int,
|
||||
itemPosition: Int
|
||||
) {
|
||||
binding.songRecycler.apply {
|
||||
(layoutManager as LinearLayoutManager).scrollToPositionWithOffset(
|
||||
itemPosition, 0
|
||||
)
|
||||
|
||||
stopScroll()
|
||||
}
|
||||
}
|
||||
) = callback(itemPosition)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
binding.songFastScrollThumb.setupWithFastScroller(binding.songFastScroll)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ val ACCENTS = arrayOf(
|
|||
* @property color The color resource for this accent
|
||||
* @property theme The theme resource for 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) {
|
||||
/**
|
||||
|
|
|
@ -34,7 +34,8 @@ fun Fragment.newMenu(anchor: View, data: BaseModel, flag: Int = ActionMenu.FLAG_
|
|||
* @param anchor [View] This should be centered around
|
||||
* @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.
|
||||
* @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(
|
||||
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)
|
||||
* 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.
|
||||
* Adapted from KAHelpers (https://github.com/FunkyMuse/KAHelpers/tree/master/viewbinding)
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class MemberBinder<T : ViewDataBinding>(
|
||||
private val fragment: Fragment,
|
||||
|
|
|
@ -17,6 +17,7 @@ import java.lang.reflect.Field
|
|||
*
|
||||
* Adapted from this StackOverflow answer:
|
||||
* https://stackoverflow.com/a/35087229
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class SlideLinearLayout @JvmOverloads constructor(
|
||||
context: Context,
|
||||
|
|
Loading…
Reference in a new issue