all: move all fragments to ViewBindingFragment
Move all fragment instances to the new ViewBindingFragment paradigm, which should help immensely with reducing memory leaks from list bindings and to really alleviate the overloaded onCreate functions.
This commit is contained in:
parent
8f38ed6ee5
commit
95057ec357
46 changed files with 875 additions and 1034 deletions
|
@ -28,14 +28,7 @@ import org.oxycblt.auxio.coil.GenreImageFetcher
|
||||||
import org.oxycblt.auxio.coil.MusicKeyer
|
import org.oxycblt.auxio.coil.MusicKeyer
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
|
|
||||||
/**
|
/** TODO: Rework RecyclerView management and item dragging */
|
||||||
* TODO: Plan for a general UI rework
|
|
||||||
* ```
|
|
||||||
* - Refactor fragment class
|
|
||||||
* - Remove databinding and dedup layouts
|
|
||||||
* - Rework RecyclerView management and item dragging
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
@Suppress("UNUSED")
|
@Suppress("UNUSED")
|
||||||
class AuxioApp : Application(), ImageLoaderFactory {
|
class AuxioApp : Application(), ImageLoaderFactory {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio
|
package org.oxycblt.auxio
|
||||||
|
|
||||||
|
/** A table containing all unique integer codes that Auxio uses. */
|
||||||
object IntegerTable {
|
object IntegerTable {
|
||||||
/** SongViewHolder */
|
/** SongViewHolder */
|
||||||
const val ITEM_TYPE_SONG = 0xA000
|
const val ITEM_TYPE_SONG = 0xA000
|
||||||
|
@ -49,7 +50,7 @@ object IntegerTable {
|
||||||
/** QueueSongViewHolder */
|
/** QueueSongViewHolder */
|
||||||
const val ITEM_TYPE_QUEUE_SONG = 0xA00D
|
const val ITEM_TYPE_QUEUE_SONG = 0xA00D
|
||||||
|
|
||||||
/** "Music playback" Notification channel */
|
/** "Music playback" Notification code */
|
||||||
const val NOTIFICATION_CODE = 0xA0A0
|
const val NOTIFICATION_CODE = 0xA0A0
|
||||||
/** Intent request code */
|
/** Intent request code */
|
||||||
const val REQUEST_CODE = 0xA0C0
|
const val REQUEST_CODE = 0xA0C0
|
||||||
|
|
|
@ -22,18 +22,18 @@ import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,31 +43,25 @@ import org.oxycblt.auxio.util.logW
|
||||||
*
|
*
|
||||||
* TODO: Add a new view with a stack trace whenever the music loading process fails.
|
* TODO: Add a new view with a stack trace whenever the music loading process fails.
|
||||||
*/
|
*/
|
||||||
class MainFragment : Fragment() {
|
class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val musicModel: MusicViewModel by activityViewModels()
|
private val musicModel: MusicViewModel by activityViewModels()
|
||||||
private var callback: Callback? = null
|
private var callback: DynamicBackPressedCallback? = null
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentMainBinding.inflate(inflater)
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val binding = FragmentMainBinding.inflate(inflater)
|
|
||||||
|
|
||||||
|
override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
|
// --- UI SETUP ---
|
||||||
// Build the permission launcher here as you can only do it in onCreateView/onCreate
|
// Build the permission launcher here as you can only do it in onCreateView/onCreate
|
||||||
val permLauncher =
|
val permLauncher =
|
||||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||||
musicModel.reloadMusic(requireContext())
|
musicModel.reloadMusic(requireContext())
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- UI SETUP ---
|
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
|
|
||||||
requireActivity()
|
requireActivity()
|
||||||
.onBackPressedDispatcher
|
.onBackPressedDispatcher
|
||||||
.addCallback(viewLifecycleOwner, Callback(binding).also { callback = it })
|
.addCallback(viewLifecycleOwner, DynamicBackPressedCallback().also { callback = it })
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
// Auxio's layout completely breaks down when it's window is resized too small,
|
// Auxio's layout completely breaks down when it's window is resized too small,
|
||||||
|
@ -87,59 +81,15 @@ class MainFragment : Fragment() {
|
||||||
|
|
||||||
// Initialize music loading. Do it here so that it shows on every fragment that this
|
// Initialize music loading. Do it here so that it shows on every fragment that this
|
||||||
// one contains.
|
// one contains.
|
||||||
|
// TODO: Move this to a service [automatic rescanning]
|
||||||
musicModel.loadMusic(requireContext())
|
musicModel.loadMusic(requireContext())
|
||||||
|
|
||||||
// Handle the music loader response.
|
// Handle the music loader response.
|
||||||
musicModel.loaderResponse.observe(viewLifecycleOwner) { response ->
|
musicModel.loaderResponse.observe(viewLifecycleOwner) { response ->
|
||||||
// Handle the loader response.
|
handleLoaderResponse(response, permLauncher)
|
||||||
when (response) {
|
|
||||||
// Ok, start restoring playback now
|
|
||||||
is MusicStore.Response.Ok -> playbackModel.setupPlayback(requireContext())
|
|
||||||
|
|
||||||
// Error, show the error to the user
|
|
||||||
is MusicStore.Response.Err -> {
|
|
||||||
logW("Received Error")
|
|
||||||
|
|
||||||
val errorRes =
|
|
||||||
when (response.kind) {
|
|
||||||
MusicStore.ErrorKind.NO_MUSIC -> R.string.err_no_music
|
|
||||||
MusicStore.ErrorKind.NO_PERMS -> R.string.err_no_perms
|
|
||||||
MusicStore.ErrorKind.FAILED -> R.string.err_load_failed
|
|
||||||
}
|
|
||||||
|
|
||||||
val snackbar =
|
|
||||||
Snackbar.make(binding.root, getString(errorRes), Snackbar.LENGTH_INDEFINITE)
|
|
||||||
|
|
||||||
when (response.kind) {
|
|
||||||
MusicStore.ErrorKind.FAILED, MusicStore.ErrorKind.NO_MUSIC -> {
|
|
||||||
snackbar.setAction(R.string.lbl_retry) {
|
|
||||||
musicModel.reloadMusic(requireContext())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MusicStore.ErrorKind.NO_PERMS -> {
|
|
||||||
snackbar.setAction(R.string.lbl_grant) {
|
|
||||||
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
snackbar.show()
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackModel.song.observe(viewLifecycleOwner) { song ->
|
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
||||||
if (song != null) {
|
|
||||||
binding.bottomSheetLayout.show()
|
|
||||||
} else {
|
|
||||||
binding.bottomSheetLayout.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logD("Fragment Created")
|
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
@ -152,12 +102,66 @@ class MainFragment : Fragment() {
|
||||||
callback?.isEnabled = false
|
callback?.isEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleLoaderResponse(
|
||||||
|
response: MusicStore.Response?,
|
||||||
|
permLauncher: ActivityResultLauncher<String>
|
||||||
|
) {
|
||||||
|
val binding = requireBinding()
|
||||||
|
|
||||||
|
// Handle the loader response.
|
||||||
|
when (response) {
|
||||||
|
// Ok, start restoring playback now
|
||||||
|
is MusicStore.Response.Ok -> playbackModel.setupPlayback(requireContext())
|
||||||
|
|
||||||
|
// Error, show the error to the user
|
||||||
|
is MusicStore.Response.Err -> {
|
||||||
|
logW("Received Error")
|
||||||
|
|
||||||
|
val errorRes =
|
||||||
|
when (response.kind) {
|
||||||
|
MusicStore.ErrorKind.NO_MUSIC -> R.string.err_no_music
|
||||||
|
MusicStore.ErrorKind.NO_PERMS -> R.string.err_no_perms
|
||||||
|
MusicStore.ErrorKind.FAILED -> R.string.err_load_failed
|
||||||
|
}
|
||||||
|
|
||||||
|
val snackbar =
|
||||||
|
Snackbar.make(binding.root, getString(errorRes), Snackbar.LENGTH_INDEFINITE)
|
||||||
|
|
||||||
|
when (response.kind) {
|
||||||
|
MusicStore.ErrorKind.FAILED, MusicStore.ErrorKind.NO_MUSIC -> {
|
||||||
|
snackbar.setAction(R.string.lbl_retry) {
|
||||||
|
musicModel.reloadMusic(requireContext())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MusicStore.ErrorKind.NO_PERMS -> {
|
||||||
|
snackbar.setAction(R.string.lbl_grant) {
|
||||||
|
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snackbar.show()
|
||||||
|
}
|
||||||
|
null -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSong(song: Song?) {
|
||||||
|
val binding = requireBinding()
|
||||||
|
if (song != null) {
|
||||||
|
binding.bottomSheetLayout.show()
|
||||||
|
} else {
|
||||||
|
binding.bottomSheetLayout.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A back press callback that handles how to respond to backwards navigation in the detail
|
* A back press callback that handles how to respond to backwards navigation in the detail
|
||||||
* fragments and the playback panel.
|
* fragments and the playback panel.
|
||||||
*/
|
*/
|
||||||
inner class Callback(private val binding: FragmentMainBinding) : OnBackPressedCallback(false) {
|
inner class DynamicBackPressedCallback : OnBackPressedCallback(false) {
|
||||||
override fun handleOnBackPressed() {
|
override fun handleOnBackPressed() {
|
||||||
|
val binding = requireBinding()
|
||||||
if (!binding.bottomSheetLayout.collapse()) {
|
if (!binding.bottomSheetLayout.collapse()) {
|
||||||
val navController = binding.exploreNavHost.findNavController()
|
val navController = binding.exploreNavHost.findNavController()
|
||||||
|
|
||||||
|
|
|
@ -19,54 +19,23 @@ package org.oxycblt.auxio.accent
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.DialogAccentBinding
|
import org.oxycblt.auxio.databinding.DialogAccentBinding
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
import org.oxycblt.auxio.ui.LifecycleDialog
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialog responsible for showing the list of accents to select.
|
* Dialog responsible for showing the list of accents to select.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class AccentCustomizeDialog : LifecycleDialog() {
|
class AccentCustomizeDialog : ViewBindingDialogFragment<DialogAccentBinding>() {
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
private var pendingAccent = settingsManager.accent
|
private var pendingAccent = settingsManager.accent
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateBinding(inflater: LayoutInflater) = DialogAccentBinding.inflate(inflater)
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val binding = DialogAccentBinding.inflate(inflater)
|
|
||||||
|
|
||||||
savedInstanceState?.getInt(KEY_PENDING_ACCENT)?.let { index ->
|
|
||||||
pendingAccent = Accent(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- UI SETUP ---
|
|
||||||
|
|
||||||
binding.accentRecycler.apply {
|
|
||||||
adapter =
|
|
||||||
AccentAdapter(pendingAccent) { accent ->
|
|
||||||
logD("Switching selected accent to $accent")
|
|
||||||
pendingAccent = accent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logD("Dialog created")
|
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
outState.putInt(KEY_PENDING_ACCENT, pendingAccent.index)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConfigDialog(builder: AlertDialog.Builder) {
|
override fun onConfigDialog(builder: AlertDialog.Builder) {
|
||||||
builder.setTitle(R.string.set_accent)
|
builder.setTitle(R.string.set_accent)
|
||||||
|
@ -85,6 +54,25 @@ class AccentCustomizeDialog : LifecycleDialog() {
|
||||||
builder.setNegativeButton(android.R.string.cancel, null)
|
builder.setNegativeButton(android.R.string.cancel, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onBindingCreated(binding: DialogAccentBinding, savedInstanceState: Bundle?) {
|
||||||
|
savedInstanceState?.getInt(KEY_PENDING_ACCENT)?.let { index ->
|
||||||
|
pendingAccent = Accent(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- UI SETUP ---
|
||||||
|
|
||||||
|
binding.accentRecycler.adapter =
|
||||||
|
AccentAdapter(pendingAccent) { accent ->
|
||||||
|
logD("Switching selected accent to $accent")
|
||||||
|
pendingAccent = accent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putInt(KEY_PENDING_ACCENT, pendingAccent.index)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = BuildConfig.APPLICATION_ID + ".tag.ACCENT_PICKER"
|
const val TAG = BuildConfig.APPLICATION_ID + ".tag.ACCENT_PICKER"
|
||||||
const val KEY_PENDING_ACCENT = BuildConfig.APPLICATION_ID + ".key.PENDING_ACCENT"
|
const val KEY_PENDING_ACCENT = BuildConfig.APPLICATION_ID + ".key.PENDING_ACCENT"
|
||||||
|
|
|
@ -58,7 +58,7 @@ abstract class BaseFetcher : Fetcher {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the artwork of an [album]. This call respects user configuration and has proper
|
* Fetch the artwork of an [album]. This call respects user configuration and has proper
|
||||||
* redundancy in the case that an API fails to load.
|
* redundancy in the case that metadata fails to load.
|
||||||
*/
|
*/
|
||||||
protected suspend fun fetchArt(context: Context, album: Album): InputStream? {
|
protected suspend fun fetchArt(context: Context, album: Album): InputStream? {
|
||||||
if (!settingsManager.showCovers) {
|
if (!settingsManager.showCovers) {
|
||||||
|
@ -77,14 +77,6 @@ abstract class BaseFetcher : Fetcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
private suspend fun fetchMediaStoreCovers(context: Context, data: Album): InputStream? {
|
|
||||||
val uri = data.albumCoverUri
|
|
||||||
|
|
||||||
// Eliminate any chance that this blocking call might mess up the cancellation process
|
|
||||||
return withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun fetchQualityCovers(context: Context, album: Album): InputStream? {
|
private suspend fun fetchQualityCovers(context: Context, album: Album): InputStream? {
|
||||||
// Loading quality covers basically means to parse the file metadata ourselves
|
// Loading quality covers basically means to parse the file metadata ourselves
|
||||||
// and then extract the cover.
|
// and then extract the cover.
|
||||||
|
@ -207,6 +199,14 @@ abstract class BaseFetcher : Fetcher {
|
||||||
return stream
|
return stream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
private suspend fun fetchMediaStoreCovers(context: Context, data: Album): InputStream? {
|
||||||
|
val uri = data.albumCoverUri
|
||||||
|
|
||||||
|
// Eliminate any chance that this blocking call might mess up the loading process
|
||||||
|
return withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a mosaic image from multiple streams of image data, Code adapted from Phonograph
|
* Create a mosaic image from multiple streams of image data, Code adapted from Phonograph
|
||||||
* https://github.com/kabouzeid/Phonograph
|
* https://github.com/kabouzeid/Phonograph
|
||||||
|
|
|
@ -39,14 +39,14 @@ class SquareFrameTransform : Transformation {
|
||||||
val x = (input.width - dstSize) / 2
|
val x = (input.width - dstSize) / 2
|
||||||
val y = (input.height - dstSize) / 2
|
val y = (input.height - dstSize) / 2
|
||||||
|
|
||||||
val wantedWidth = size.width.pxOrElse { dstSize }
|
val desiredWidth = size.width.pxOrElse { dstSize }
|
||||||
val wantedHeight = size.height.pxOrElse { dstSize }
|
val desiredHeight = size.height.pxOrElse { dstSize }
|
||||||
|
|
||||||
val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
|
val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
|
||||||
|
|
||||||
if (dstSize != wantedWidth || dstSize != wantedHeight) {
|
if (dstSize != desiredWidth || dstSize != desiredHeight) {
|
||||||
// Desired size differs from the cropped size, resize the bitmap.
|
// Desired size differs from the cropped size, resize the bitmap.
|
||||||
return Bitmap.createScaledBitmap(dst, wantedWidth, wantedHeight, true)
|
return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return dst
|
return dst
|
||||||
|
|
|
@ -19,9 +19,6 @@ package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
|
@ -33,6 +30,7 @@ import org.oxycblt.auxio.music.ActionHeader
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Header
|
import org.oxycblt.auxio.music.Header
|
||||||
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
import org.oxycblt.auxio.playback.state.PlaybackMode
|
||||||
import org.oxycblt.auxio.ui.ActionMenu
|
import org.oxycblt.auxio.ui.ActionMenu
|
||||||
|
@ -49,14 +47,9 @@ import org.oxycblt.auxio.util.showToast
|
||||||
class AlbumDetailFragment : DetailFragment() {
|
class AlbumDetailFragment : DetailFragment() {
|
||||||
private val args: AlbumDetailFragmentArgs by navArgs()
|
private val args: AlbumDetailFragmentArgs by navArgs()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||||
inflater: LayoutInflater,
|
detailModel.setAlbumId(args.albumId)
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
detailModel.setAlbum(args.albumId)
|
|
||||||
|
|
||||||
val binding = FragmentDetailBinding.inflate(layoutInflater)
|
|
||||||
val detailAdapter =
|
val detailAdapter =
|
||||||
AlbumDetailAdapter(
|
AlbumDetailAdapter(
|
||||||
playbackModel,
|
playbackModel,
|
||||||
|
@ -64,11 +57,7 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
doOnClick = { playbackModel.playSong(it, PlaybackMode.IN_ALBUM) },
|
doOnClick = { playbackModel.playSong(it, PlaybackMode.IN_ALBUM) },
|
||||||
doOnLongClick = { view, data -> newMenu(view, data, ActionMenu.FLAG_IN_ALBUM) })
|
doOnLongClick = { view, data -> newMenu(view, data, ActionMenu.FLAG_IN_ALBUM) })
|
||||||
|
|
||||||
// --- UI SETUP ---
|
setupToolbar(detailModel.curAlbum.value!!, R.menu.menu_album_detail) { itemId ->
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
|
|
||||||
setupToolbar(detailModel.curAlbum.value!!, binding, R.menu.menu_album_detail) { itemId ->
|
|
||||||
when (itemId) {
|
when (itemId) {
|
||||||
R.id.action_play_next -> {
|
R.id.action_play_next -> {
|
||||||
playbackModel.playNext(detailModel.curAlbum.value!!)
|
playbackModel.playNext(detailModel.curAlbum.value!!)
|
||||||
|
@ -84,16 +73,14 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupRecycler(binding, detailAdapter) { pos ->
|
setupRecycler(detailAdapter) { pos ->
|
||||||
val item = detailAdapter.currentList[pos]
|
val item = detailAdapter.currentList[pos]
|
||||||
item is Header || item is ActionHeader || item is Album
|
item is Header || item is ActionHeader || item is Album
|
||||||
}
|
}
|
||||||
|
|
||||||
updateQueueActions(playbackModel.song.value, binding)
|
// -- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
// -- DETAILVIEWMODEL SETUP ---
|
detailModel.albumData.observe(viewLifecycleOwner, detailAdapter::submitList)
|
||||||
|
|
||||||
detailModel.albumData.observe(viewLifecycleOwner) { data -> detailAdapter.submitList(data) }
|
|
||||||
|
|
||||||
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
|
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
|
||||||
if (config != null) {
|
if (config != null) {
|
||||||
|
@ -102,84 +89,62 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
||||||
when (item) {
|
handleNavigation(item, detailAdapter)
|
||||||
// Songs should be scrolled to if the album matches, or a new detail
|
|
||||||
// fragment should be launched otherwise.
|
|
||||||
is Song -> {
|
|
||||||
if (detailModel.curAlbum.value!!.id == item.album.id) {
|
|
||||||
logD("Navigating to a song in this album")
|
|
||||||
scrollToItem(item.id, binding, detailAdapter)
|
|
||||||
detailModel.finishNavToItem()
|
|
||||||
} else {
|
|
||||||
logD("Navigating to another album")
|
|
||||||
findNavController()
|
|
||||||
.navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.album.id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the album matches, no need to do anything. Otherwise launch a new
|
|
||||||
// detail fragment.
|
|
||||||
is Album -> {
|
|
||||||
if (detailModel.curAlbum.value!!.id == item.id) {
|
|
||||||
logD("Navigating to the top of this album")
|
|
||||||
binding.detailRecycler.scrollToPosition(0)
|
|
||||||
detailModel.finishNavToItem()
|
|
||||||
} else {
|
|
||||||
logD("Navigating to another album")
|
|
||||||
findNavController()
|
|
||||||
.navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always launch a new ArtistDetailFragment.
|
|
||||||
is Artist -> {
|
|
||||||
logD("Navigating to another artist")
|
|
||||||
findNavController()
|
|
||||||
.navigate(AlbumDetailFragmentDirections.actionShowArtist(item.id))
|
|
||||||
}
|
|
||||||
null -> {}
|
|
||||||
else -> logW("Unsupported navigation item ${item::class.java}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- PLAYBACKVIEWMODEL SETUP ---
|
updateSong(playbackModel.song.value, detailAdapter)
|
||||||
|
playbackModel.song.observe(viewLifecycleOwner) { song -> updateSong(song, detailAdapter) }
|
||||||
playbackModel.song.observe(viewLifecycleOwner) { song ->
|
|
||||||
updateQueueActions(song, binding)
|
|
||||||
|
|
||||||
if (playbackModel.playbackMode.value == PlaybackMode.IN_ALBUM &&
|
|
||||||
playbackModel.parent.value?.id == detailModel.curAlbum.value!!.id) {
|
|
||||||
detailAdapter.highlightSong(song, binding.detailRecycler)
|
|
||||||
} else {
|
|
||||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
|
||||||
detailAdapter.highlightSong(null, binding.detailRecycler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logD("Fragment created")
|
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Updates the queue actions when */
|
private fun handleNavigation(item: Music?, adapter: AlbumDetailAdapter) {
|
||||||
private fun updateQueueActions(song: Song?, binding: FragmentDetailBinding) {
|
val binding = requireBinding()
|
||||||
for (item in binding.detailToolbar.menu.children) {
|
when (item) {
|
||||||
if (item.itemId == R.id.action_play_next || item.itemId == R.id.action_queue_add) {
|
// Songs should be scrolled to if the album matches, or a new detail
|
||||||
item.isEnabled = song != null
|
// fragment should be launched otherwise.
|
||||||
|
is Song -> {
|
||||||
|
if (detailModel.curAlbum.value!!.id == item.album.id) {
|
||||||
|
logD("Navigating to a song in this album")
|
||||||
|
scrollToItem(item.id, adapter)
|
||||||
|
detailModel.finishNavToItem()
|
||||||
|
} else {
|
||||||
|
logD("Navigating to another album")
|
||||||
|
findNavController()
|
||||||
|
.navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.album.id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the album matches, no need to do anything. Otherwise launch a new
|
||||||
|
// detail fragment.
|
||||||
|
is Album -> {
|
||||||
|
if (detailModel.curAlbum.value!!.id == item.id) {
|
||||||
|
logD("Navigating to the top of this album")
|
||||||
|
binding.detailRecycler.scrollToPosition(0)
|
||||||
|
detailModel.finishNavToItem()
|
||||||
|
} else {
|
||||||
|
logD("Navigating to another album")
|
||||||
|
findNavController()
|
||||||
|
.navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always launch a new ArtistDetailFragment.
|
||||||
|
is Artist -> {
|
||||||
|
logD("Navigating to another artist")
|
||||||
|
findNavController()
|
||||||
|
.navigate(AlbumDetailFragmentDirections.actionShowArtist(item.id))
|
||||||
|
}
|
||||||
|
null -> {}
|
||||||
|
else -> logW("Unsupported navigation item ${item::class.java}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Scroll to an song using its [id]. */
|
/** Scroll to an song using its [id]. */
|
||||||
private fun scrollToItem(
|
private fun scrollToItem(id: Long, adapter: AlbumDetailAdapter) {
|
||||||
id: Long,
|
|
||||||
binding: FragmentDetailBinding,
|
|
||||||
adapter: AlbumDetailAdapter
|
|
||||||
) {
|
|
||||||
// Calculate where the item for the currently played song is
|
// Calculate where the item for the currently played song is
|
||||||
val pos = adapter.currentList.indexOfFirst { it.id == id && it is Song }
|
val pos = adapter.currentList.indexOfFirst { it.id == id && it is Song }
|
||||||
|
|
||||||
if (pos != -1) {
|
if (pos != -1) {
|
||||||
|
val binding = requireBinding()
|
||||||
binding.detailRecycler.post {
|
binding.detailRecycler.post {
|
||||||
// Make sure to increment the position to make up for the detail header
|
// Make sure to increment the position to make up for the detail header
|
||||||
binding.detailRecycler.layoutManager?.startSmoothScroll(
|
binding.detailRecycler.layoutManager?.startSmoothScroll(
|
||||||
|
@ -193,6 +158,25 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Updates the queue actions when a song is present or not */
|
||||||
|
private fun updateSong(song: Song?, adapter: AlbumDetailAdapter) {
|
||||||
|
val binding = requireBinding()
|
||||||
|
|
||||||
|
for (item in binding.detailToolbar.menu.children) {
|
||||||
|
if (item.itemId == R.id.action_play_next || item.itemId == R.id.action_queue_add) {
|
||||||
|
item.isEnabled = song != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playbackModel.playbackMode.value == PlaybackMode.IN_ALBUM &&
|
||||||
|
playbackModel.parent.value?.id == detailModel.curAlbum.value!!.id) {
|
||||||
|
adapter.highlightSong(song, binding.detailRecycler)
|
||||||
|
} else {
|
||||||
|
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||||
|
adapter.highlightSong(null, binding.detailRecycler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [LinearSmoothScroller] subclass that centers the item on the screen instead of snapping to
|
* [LinearSmoothScroller] subclass that centers the item on the screen instead of snapping to
|
||||||
* the top or bottom.
|
* the top or bottom.
|
||||||
|
|
|
@ -18,9 +18,6 @@
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
@ -30,6 +27,8 @@ import org.oxycblt.auxio.music.ActionHeader
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Header
|
import org.oxycblt.auxio.music.Header
|
||||||
|
import org.oxycblt.auxio.music.Music
|
||||||
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
import org.oxycblt.auxio.playback.state.PlaybackMode
|
||||||
import org.oxycblt.auxio.ui.ActionMenu
|
import org.oxycblt.auxio.ui.ActionMenu
|
||||||
|
@ -44,21 +43,15 @@ import org.oxycblt.auxio.util.logW
|
||||||
class ArtistDetailFragment : DetailFragment() {
|
class ArtistDetailFragment : DetailFragment() {
|
||||||
private val args: ArtistDetailFragmentArgs by navArgs()
|
private val args: ArtistDetailFragmentArgs by navArgs()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||||
inflater: LayoutInflater,
|
detailModel.setArtistId(args.artistId)
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
detailModel.setArtist(args.artistId)
|
|
||||||
|
|
||||||
val binding = FragmentDetailBinding.inflate(layoutInflater)
|
|
||||||
val detailAdapter =
|
val detailAdapter =
|
||||||
ArtistDetailAdapter(
|
ArtistDetailAdapter(
|
||||||
playbackModel,
|
playbackModel,
|
||||||
doOnClick = { data ->
|
doOnClick = { data ->
|
||||||
if (!detailModel.isNavigating) {
|
if (!detailModel.isNavigating) {
|
||||||
detailModel.setNavigating(true)
|
detailModel.setNavigating(true)
|
||||||
|
|
||||||
findNavController()
|
findNavController()
|
||||||
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(data.id))
|
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(data.id))
|
||||||
}
|
}
|
||||||
|
@ -66,12 +59,8 @@ class ArtistDetailFragment : DetailFragment() {
|
||||||
doOnSongClick = { data -> playbackModel.playSong(data, PlaybackMode.IN_ARTIST) },
|
doOnSongClick = { data -> playbackModel.playSong(data, PlaybackMode.IN_ARTIST) },
|
||||||
doOnLongClick = { view, data -> newMenu(view, data, ActionMenu.FLAG_IN_ARTIST) })
|
doOnLongClick = { view, data -> newMenu(view, data, ActionMenu.FLAG_IN_ARTIST) })
|
||||||
|
|
||||||
// --- UI SETUP ---
|
setupToolbar(detailModel.currentArtist.value!!)
|
||||||
|
setupRecycler(detailAdapter) { pos ->
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
|
|
||||||
setupToolbar(detailModel.curArtist.value!!, binding)
|
|
||||||
setupRecycler(binding, detailAdapter) { pos ->
|
|
||||||
// If the item is an ActionHeader we need to also make the item full-width
|
// If the item is an ActionHeader we need to also make the item full-width
|
||||||
val item = detailAdapter.currentList[pos]
|
val item = detailAdapter.currentList[pos]
|
||||||
item is Header || item is ActionHeader || item is Artist
|
item is Header || item is ActionHeader || item is Artist
|
||||||
|
@ -79,9 +68,7 @@ class ArtistDetailFragment : DetailFragment() {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
detailModel.artistData.observe(viewLifecycleOwner) { data ->
|
detailModel.artistData.observe(viewLifecycleOwner, detailAdapter::submitList)
|
||||||
detailAdapter.submitList(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
|
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
|
||||||
if (config != null) {
|
if (config != null) {
|
||||||
|
@ -89,56 +76,64 @@ class ArtistDetailFragment : DetailFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
detailModel.navToItem.observe(viewLifecycleOwner, ::handleNavigation)
|
||||||
when (item) {
|
|
||||||
is Artist -> {
|
// Highlight songs if they are being played
|
||||||
if (item.id == detailModel.curArtist.value?.id) {
|
playbackModel.song.observe(viewLifecycleOwner) { song -> updateSong(song, detailAdapter) }
|
||||||
logD("Navigating to the top of this artist")
|
|
||||||
binding.detailRecycler.scrollToPosition(0)
|
|
||||||
detailModel.finishNavToItem()
|
|
||||||
} else {
|
|
||||||
logD("Navigating to another artist")
|
|
||||||
findNavController()
|
|
||||||
.navigate(ArtistDetailFragmentDirections.actionShowArtist(item.id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is Album -> {
|
|
||||||
logD("Navigating to another album")
|
|
||||||
findNavController()
|
|
||||||
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.id))
|
|
||||||
}
|
|
||||||
is Song -> {
|
|
||||||
logD("Navigating to another album")
|
|
||||||
findNavController()
|
|
||||||
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.album.id))
|
|
||||||
}
|
|
||||||
null -> {}
|
|
||||||
else -> logW("Unsupported navigation item ${item::class.java}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Highlight albums if they are being played
|
// Highlight albums if they are being played
|
||||||
playbackModel.parent.observe(viewLifecycleOwner) { parent ->
|
playbackModel.parent.observe(viewLifecycleOwner) { parent ->
|
||||||
if (parent is Album?) {
|
updateParent(parent, detailAdapter)
|
||||||
detailAdapter.highlightAlbum(parent, binding.detailRecycler)
|
|
||||||
} else {
|
|
||||||
detailAdapter.highlightAlbum(null, binding.detailRecycler)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Highlight songs if they are being played
|
private fun handleNavigation(item: Music?) {
|
||||||
playbackModel.song.observe(viewLifecycleOwner) { song ->
|
val binding = requireBinding()
|
||||||
if (playbackModel.playbackMode.value == PlaybackMode.IN_ARTIST &&
|
|
||||||
playbackModel.parent.value?.id == detailModel.curArtist.value?.id) {
|
when (item) {
|
||||||
detailAdapter.highlightSong(song, binding.detailRecycler)
|
is Artist -> {
|
||||||
} else {
|
if (item.id == detailModel.currentArtist.value?.id) {
|
||||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
logD("Navigating to the top of this artist")
|
||||||
detailAdapter.highlightSong(null, binding.detailRecycler)
|
binding.detailRecycler.scrollToPosition(0)
|
||||||
|
detailModel.finishNavToItem()
|
||||||
|
} else {
|
||||||
|
logD("Navigating to another artist")
|
||||||
|
findNavController()
|
||||||
|
.navigate(ArtistDetailFragmentDirections.actionShowArtist(item.id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
is Album -> {
|
||||||
|
logD("Navigating to another album")
|
||||||
|
findNavController()
|
||||||
|
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.id))
|
||||||
|
}
|
||||||
|
is Song -> {
|
||||||
|
logD("Navigating to another album")
|
||||||
|
findNavController()
|
||||||
|
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.album.id))
|
||||||
|
}
|
||||||
|
null -> {}
|
||||||
|
else -> logW("Unsupported navigation item ${item::class.java}")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logD("Fragment created")
|
private fun updateSong(song: Song?, adapter: ArtistDetailAdapter) {
|
||||||
|
val binding = requireBinding()
|
||||||
|
if (playbackModel.playbackMode.value == PlaybackMode.IN_ARTIST &&
|
||||||
|
playbackModel.parent.value?.id == detailModel.currentArtist.value?.id) {
|
||||||
|
adapter.highlightSong(song, binding.detailRecycler)
|
||||||
|
} else {
|
||||||
|
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||||
|
adapter.highlightSong(null, binding.detailRecycler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return binding.root
|
private fun updateParent(parent: MusicParent?, adapter: ArtistDetailAdapter) {
|
||||||
|
val binding = requireBinding()
|
||||||
|
if (parent is Album?) {
|
||||||
|
adapter.highlightAlbum(parent, binding.detailRecycler)
|
||||||
|
} else {
|
||||||
|
adapter.highlightAlbum(null, binding.detailRecycler)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
import androidx.annotation.MenuRes
|
import androidx.annotation.MenuRes
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
|
@ -28,6 +29,7 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.applySpans
|
import org.oxycblt.auxio.util.applySpans
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
@ -35,10 +37,13 @@ import org.oxycblt.auxio.util.logD
|
||||||
* A Base [Fragment] implementing the base features shared across all detail fragments.
|
* A Base [Fragment] implementing the base features shared across all detail fragments.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
abstract class DetailFragment : Fragment() {
|
abstract class DetailFragment : ViewBindingFragment<FragmentDetailBinding>() {
|
||||||
protected val detailModel: DetailViewModel by activityViewModels()
|
protected val detailModel: DetailViewModel by activityViewModels()
|
||||||
protected val playbackModel: PlaybackViewModel by activityViewModels()
|
protected val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreateBinding(inflater: LayoutInflater): FragmentDetailBinding =
|
||||||
|
FragmentDetailBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
detailModel.setNavigating(false)
|
detailModel.setNavigating(false)
|
||||||
|
@ -58,11 +63,10 @@ abstract class DetailFragment : Fragment() {
|
||||||
*/
|
*/
|
||||||
protected fun setupToolbar(
|
protected fun setupToolbar(
|
||||||
data: MusicParent,
|
data: MusicParent,
|
||||||
binding: FragmentDetailBinding,
|
|
||||||
@MenuRes menuId: Int = -1,
|
@MenuRes menuId: Int = -1,
|
||||||
onMenuClick: ((itemId: Int) -> Boolean)? = null
|
onMenuClick: ((itemId: Int) -> Boolean)? = null
|
||||||
) {
|
) {
|
||||||
binding.detailToolbar.apply {
|
requireBinding().detailToolbar.apply {
|
||||||
title = data.resolvedName
|
title = data.resolvedName
|
||||||
|
|
||||||
if (menuId != -1) {
|
if (menuId != -1) {
|
||||||
|
@ -79,11 +83,10 @@ abstract class DetailFragment : Fragment() {
|
||||||
|
|
||||||
/** Shortcut method for recyclerview setup */
|
/** Shortcut method for recyclerview setup */
|
||||||
protected fun setupRecycler(
|
protected fun setupRecycler(
|
||||||
binding: FragmentDetailBinding,
|
|
||||||
detailAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>,
|
detailAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>,
|
||||||
gridLookup: (Int) -> Boolean
|
gridLookup: (Int) -> Boolean
|
||||||
) {
|
) {
|
||||||
binding.detailRecycler.apply {
|
requireBinding().detailRecycler.apply {
|
||||||
adapter = detailAdapter
|
adapter = detailAdapter
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
applySpans(gridLookup)
|
applySpans(gridLookup)
|
||||||
|
|
|
@ -28,6 +28,7 @@ import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Header
|
import org.oxycblt.auxio.music.Header
|
||||||
import org.oxycblt.auxio.music.Item
|
import org.oxycblt.auxio.music.Item
|
||||||
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
|
@ -43,39 +44,37 @@ import org.oxycblt.auxio.util.logD
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class DetailViewModel : ViewModel() {
|
class DetailViewModel : ViewModel() {
|
||||||
// --- CURRENT VALUES ---
|
private val mCurrentAlbum = MutableLiveData<Album?>()
|
||||||
|
|
||||||
private val mCurGenre = MutableLiveData<Genre?>()
|
|
||||||
val curGenre: LiveData<Genre?>
|
|
||||||
get() = mCurGenre
|
|
||||||
|
|
||||||
private val mGenreData = MutableLiveData(listOf<Item>())
|
|
||||||
val genreData: LiveData<List<Item>> = mGenreData
|
|
||||||
|
|
||||||
private val mCurArtist = MutableLiveData<Artist?>()
|
|
||||||
val curArtist: LiveData<Artist?>
|
|
||||||
get() = mCurArtist
|
|
||||||
|
|
||||||
private val mArtistData = MutableLiveData(listOf<Item>())
|
|
||||||
val artistData: LiveData<List<Item>> = mArtistData
|
|
||||||
|
|
||||||
private val mCurAlbum = MutableLiveData<Album?>()
|
|
||||||
val curAlbum: LiveData<Album?>
|
val curAlbum: LiveData<Album?>
|
||||||
get() = mCurAlbum
|
get() = mCurrentAlbum
|
||||||
|
|
||||||
private val mAlbumData = MutableLiveData(listOf<Item>())
|
private val mAlbumData = MutableLiveData(listOf<Item>())
|
||||||
val albumData: LiveData<List<Item>>
|
val albumData: LiveData<List<Item>>
|
||||||
get() = mAlbumData
|
get() = mAlbumData
|
||||||
|
|
||||||
|
private val mCurrentArtist = MutableLiveData<Artist?>()
|
||||||
|
val currentArtist: LiveData<Artist?>
|
||||||
|
get() = mCurrentArtist
|
||||||
|
|
||||||
|
private val mArtistData = MutableLiveData(listOf<Item>())
|
||||||
|
val artistData: LiveData<List<Item>> = mArtistData
|
||||||
|
|
||||||
|
private val mCurrentGenre = MutableLiveData<Genre?>()
|
||||||
|
val currentGenre: LiveData<Genre?>
|
||||||
|
get() = mCurrentGenre
|
||||||
|
|
||||||
|
private val mGenreData = MutableLiveData(listOf<Item>())
|
||||||
|
val genreData: LiveData<List<Item>> = mGenreData
|
||||||
|
|
||||||
data class MenuConfig(val anchor: View, val sortMode: Sort)
|
data class MenuConfig(val anchor: View, val sortMode: Sort)
|
||||||
|
|
||||||
private val mShowMenu = MutableLiveData<MenuConfig?>(null)
|
private val mShowMenu = MutableLiveData<MenuConfig?>(null)
|
||||||
val showMenu: LiveData<MenuConfig?> = mShowMenu
|
val showMenu: LiveData<MenuConfig?> = mShowMenu
|
||||||
|
|
||||||
private val mNavToItem = MutableLiveData<Item?>()
|
private val mNavToItem = MutableLiveData<Music?>()
|
||||||
|
|
||||||
/** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */
|
/** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */
|
||||||
val navToItem: LiveData<Item?>
|
val navToItem: LiveData<Music?>
|
||||||
get() = mNavToItem
|
get() = mNavToItem
|
||||||
|
|
||||||
var isNavigating = false
|
var isNavigating = false
|
||||||
|
@ -84,25 +83,25 @@ class DetailViewModel : ViewModel() {
|
||||||
private var currentMenuContext: DisplayMode? = null
|
private var currentMenuContext: DisplayMode? = null
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
fun setGenre(id: Long) {
|
fun setAlbumId(id: Long) {
|
||||||
if (mCurGenre.value?.id == id) return
|
if (mCurrentAlbum.value?.id == id) return
|
||||||
val musicStore = MusicStore.requireInstance()
|
val musicStore = MusicStore.requireInstance()
|
||||||
mCurGenre.value = musicStore.genres.find { it.id == id }
|
mCurrentAlbum.value = musicStore.albums.find { it.id == id }
|
||||||
refreshGenreData()
|
refreshAlbumData()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setArtist(id: Long) {
|
fun setArtistId(id: Long) {
|
||||||
if (mCurArtist.value?.id == id) return
|
if (mCurrentArtist.value?.id == id) return
|
||||||
val musicStore = MusicStore.requireInstance()
|
val musicStore = MusicStore.requireInstance()
|
||||||
mCurArtist.value = musicStore.artists.find { it.id == id }
|
mCurrentArtist.value = musicStore.artists.find { it.id == id }
|
||||||
refreshArtistData()
|
refreshArtistData()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAlbum(id: Long) {
|
fun setGenreId(id: Long) {
|
||||||
if (mCurAlbum.value?.id == id) return
|
if (mCurrentGenre.value?.id == id) return
|
||||||
val musicStore = MusicStore.requireInstance()
|
val musicStore = MusicStore.requireInstance()
|
||||||
mCurAlbum.value = musicStore.albums.find { it.id == id }
|
mCurrentGenre.value = musicStore.genres.find { it.id == id }
|
||||||
refreshAlbumData()
|
refreshGenreData()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mark that the menu process is done with the new [Sort]. Pass null if there was no change. */
|
/** Mark that the menu process is done with the new [Sort]. Pass null if there was no change. */
|
||||||
|
@ -132,7 +131,7 @@ class DetailViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Navigate to an item, whether a song/album/artist */
|
/** Navigate to an item, whether a song/album/artist */
|
||||||
fun navToItem(item: Item) {
|
fun navToItem(item: Music) {
|
||||||
mNavToItem.value = item
|
mNavToItem.value = item
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,7 +147,7 @@ class DetailViewModel : ViewModel() {
|
||||||
|
|
||||||
private fun refreshGenreData() {
|
private fun refreshGenreData() {
|
||||||
logD("Refreshing genre data")
|
logD("Refreshing genre data")
|
||||||
val genre = requireNotNull(curGenre.value)
|
val genre = requireNotNull(currentGenre.value)
|
||||||
val data = mutableListOf<Item>(genre)
|
val data = mutableListOf<Item>(genre)
|
||||||
|
|
||||||
data.add(
|
data.add(
|
||||||
|
@ -162,14 +161,14 @@ class DetailViewModel : ViewModel() {
|
||||||
mShowMenu.value = MenuConfig(view, settingsManager.detailGenreSort)
|
mShowMenu.value = MenuConfig(view, settingsManager.detailGenreSort)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
data.addAll(settingsManager.detailGenreSort.genre(curGenre.value!!))
|
data.addAll(settingsManager.detailGenreSort.genre(currentGenre.value!!))
|
||||||
|
|
||||||
mGenreData.value = data
|
mGenreData.value = data
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshArtistData() {
|
private fun refreshArtistData() {
|
||||||
logD("Refreshing artist data")
|
logD("Refreshing artist data")
|
||||||
val artist = requireNotNull(curArtist.value)
|
val artist = requireNotNull(currentArtist.value)
|
||||||
val data = mutableListOf<Item>(artist)
|
val data = mutableListOf<Item>(artist)
|
||||||
|
|
||||||
data.add(Header(id = -2, string = R.string.lbl_albums))
|
data.add(Header(id = -2, string = R.string.lbl_albums))
|
||||||
|
|
|
@ -18,9 +18,6 @@
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
|
@ -30,6 +27,7 @@ import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Header
|
import org.oxycblt.auxio.music.Header
|
||||||
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
import org.oxycblt.auxio.playback.state.PlaybackMode
|
||||||
import org.oxycblt.auxio.ui.ActionMenu
|
import org.oxycblt.auxio.ui.ActionMenu
|
||||||
|
@ -44,77 +42,66 @@ import org.oxycblt.auxio.util.logW
|
||||||
class GenreDetailFragment : DetailFragment() {
|
class GenreDetailFragment : DetailFragment() {
|
||||||
private val args: GenreDetailFragmentArgs by navArgs()
|
private val args: GenreDetailFragmentArgs by navArgs()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||||
inflater: LayoutInflater,
|
detailModel.setGenreId(args.genreId)
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
detailModel.setGenre(args.genreId)
|
|
||||||
|
|
||||||
val binding = FragmentDetailBinding.inflate(inflater)
|
|
||||||
val detailAdapter =
|
val detailAdapter =
|
||||||
GenreDetailAdapter(
|
GenreDetailAdapter(
|
||||||
playbackModel,
|
playbackModel,
|
||||||
doOnClick = { song -> playbackModel.playSong(song, PlaybackMode.IN_GENRE) },
|
doOnClick = { song -> playbackModel.playSong(song, PlaybackMode.IN_GENRE) },
|
||||||
doOnLongClick = { view, data -> newMenu(view, data, ActionMenu.FLAG_IN_GENRE) })
|
doOnLongClick = { view, data -> newMenu(view, data, ActionMenu.FLAG_IN_GENRE) })
|
||||||
|
|
||||||
// --- UI SETUP ---
|
setupToolbar(detailModel.currentGenre.value!!)
|
||||||
|
setupRecycler(detailAdapter) { pos ->
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
|
|
||||||
setupToolbar(detailModel.curGenre.value!!, binding)
|
|
||||||
setupRecycler(binding, detailAdapter) { pos ->
|
|
||||||
val item = detailAdapter.currentList[pos]
|
val item = detailAdapter.currentList[pos]
|
||||||
item is Header || item is ActionHeader || item is Genre
|
item is Header || item is ActionHeader || item is Genre
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- DETAILVIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
detailModel.genreData.observe(viewLifecycleOwner) { data -> detailAdapter.submitList(data) }
|
detailModel.genreData.observe(viewLifecycleOwner, detailAdapter::submitList)
|
||||||
|
|
||||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
detailModel.navToItem.observe(viewLifecycleOwner, ::handleNavigation)
|
||||||
when (item) {
|
|
||||||
// All items will launch new detail fragments.
|
|
||||||
is Artist -> {
|
|
||||||
logD("Navigating to another artist")
|
|
||||||
findNavController()
|
|
||||||
.navigate(GenreDetailFragmentDirections.actionShowArtist(item.id))
|
|
||||||
}
|
|
||||||
is Album -> {
|
|
||||||
logD("Navigating to another album")
|
|
||||||
findNavController()
|
|
||||||
.navigate(GenreDetailFragmentDirections.actionShowAlbum(item.id))
|
|
||||||
}
|
|
||||||
is Song -> {
|
|
||||||
logD("Navigating to another song")
|
|
||||||
findNavController()
|
|
||||||
.navigate(GenreDetailFragmentDirections.actionShowAlbum(item.album.id))
|
|
||||||
}
|
|
||||||
null -> {}
|
|
||||||
else -> logW("Unsupported navigation command ${item::class.java}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- PLAYBACKVIEWMODEL SETUP ---
|
playbackModel.song.observe(viewLifecycleOwner) { song -> updateSong(song, detailAdapter) }
|
||||||
|
|
||||||
playbackModel.song.observe(viewLifecycleOwner) { song ->
|
|
||||||
if (playbackModel.playbackMode.value == PlaybackMode.IN_GENRE &&
|
|
||||||
playbackModel.parent.value?.id == detailModel.curGenre.value!!.id) {
|
|
||||||
detailAdapter.highlightSong(song, binding.detailRecycler)
|
|
||||||
} else {
|
|
||||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
|
||||||
detailAdapter.highlightSong(null, binding.detailRecycler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
|
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
|
||||||
if (config != null) {
|
if (config != null) {
|
||||||
showMenu(config)
|
showMenu(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logD("Fragment created")
|
private fun handleNavigation(item: Music?) {
|
||||||
|
when (item) {
|
||||||
|
// All items will launch new detail fragments.
|
||||||
|
is Artist -> {
|
||||||
|
logD("Navigating to another artist")
|
||||||
|
findNavController()
|
||||||
|
.navigate(GenreDetailFragmentDirections.actionShowArtist(item.id))
|
||||||
|
}
|
||||||
|
is Album -> {
|
||||||
|
logD("Navigating to another album")
|
||||||
|
findNavController().navigate(GenreDetailFragmentDirections.actionShowAlbum(item.id))
|
||||||
|
}
|
||||||
|
is Song -> {
|
||||||
|
logD("Navigating to another song")
|
||||||
|
findNavController()
|
||||||
|
.navigate(GenreDetailFragmentDirections.actionShowAlbum(item.album.id))
|
||||||
|
}
|
||||||
|
null -> {}
|
||||||
|
else -> logW("Unsupported navigation command ${item::class.java}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return binding.root
|
private fun updateSong(song: Song?, adapter: GenreDetailAdapter) {
|
||||||
|
val binding = requireBinding()
|
||||||
|
if (playbackModel.playbackMode.value == PlaybackMode.IN_GENRE &&
|
||||||
|
playbackModel.parent.value?.id == detailModel.currentGenre.value!!.id) {
|
||||||
|
adapter.highlightSong(song, binding.detailRecycler)
|
||||||
|
} else {
|
||||||
|
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||||
|
adapter.highlightSong(null, binding.detailRecycler)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,6 @@ package org.oxycblt.auxio.home
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.iterator
|
import androidx.core.view.iterator
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
@ -41,11 +39,13 @@ import org.oxycblt.auxio.home.list.SongListFragment
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
import org.oxycblt.auxio.util.logTraceOrThrow
|
import org.oxycblt.auxio.util.logTraceOrThrow
|
||||||
|
@ -59,104 +59,35 @@ import org.oxycblt.auxio.util.logTraceOrThrow
|
||||||
*
|
*
|
||||||
* TODO: Add duration and song count sorts
|
* TODO: Add duration and song count sorts
|
||||||
*/
|
*/
|
||||||
class HomeFragment : Fragment() {
|
class HomeFragment : ViewBindingFragment<FragmentHomeBinding>() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
private val musicModel: MusicViewModel by activityViewModels()
|
private val musicModel: MusicViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentHomeBinding.inflate(inflater)
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) {
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val binding = FragmentHomeBinding.inflate(inflater)
|
|
||||||
val sortItem: MenuItem
|
val sortItem: MenuItem
|
||||||
|
|
||||||
// --- UI SETUP ---
|
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
|
|
||||||
binding.homeToolbar.apply {
|
binding.homeToolbar.apply {
|
||||||
|
sortItem = menu.findItem(R.id.submenu_sorting)
|
||||||
|
|
||||||
setOnMenuItemClickListener { item ->
|
setOnMenuItemClickListener { item ->
|
||||||
when (item.itemId) {
|
onMenuClick(item)
|
||||||
R.id.action_search -> {
|
|
||||||
logD("Navigating to search")
|
|
||||||
findNavController().navigate(HomeFragmentDirections.actionShowSearch())
|
|
||||||
}
|
|
||||||
R.id.action_settings -> {
|
|
||||||
logD("Navigating to settings")
|
|
||||||
parentFragment
|
|
||||||
?.parentFragment
|
|
||||||
?.findNavController()
|
|
||||||
?.navigate(MainFragmentDirections.actionShowSettings())
|
|
||||||
}
|
|
||||||
R.id.action_about -> {
|
|
||||||
logD("Navigating to about")
|
|
||||||
parentFragment
|
|
||||||
?.parentFragment
|
|
||||||
?.findNavController()
|
|
||||||
?.navigate(MainFragmentDirections.actionShowAbout())
|
|
||||||
}
|
|
||||||
R.id.submenu_sorting -> {}
|
|
||||||
R.id.option_sort_asc -> {
|
|
||||||
item.isChecked = !item.isChecked
|
|
||||||
val new =
|
|
||||||
homeModel
|
|
||||||
.getSortForDisplay(homeModel.curTab.value!!)
|
|
||||||
.ascending(item.isChecked)
|
|
||||||
homeModel.updateCurrentSort(new)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sorting option was selected, mark it as selected and update the mode
|
|
||||||
else -> {
|
|
||||||
item.isChecked = true
|
|
||||||
val new =
|
|
||||||
homeModel
|
|
||||||
.getSortForDisplay(homeModel.curTab.value!!)
|
|
||||||
.assignId(item.itemId)
|
|
||||||
homeModel.updateCurrentSort(requireNotNull(new))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
sortItem = menu.findItem(R.id.submenu_sorting)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.homePager.apply {
|
binding.homePager.apply {
|
||||||
adapter = HomePagerAdapter()
|
adapter = HomePagerAdapter()
|
||||||
|
|
||||||
// By default, ViewPager2's sensitivity is high enough to result in vertical
|
|
||||||
// scroll events being registered as horizontal scroll events. Reflect into the
|
|
||||||
// internal recyclerview and change the touch slope so that touch actions will
|
|
||||||
// act more as a scroll than as a swipe.
|
|
||||||
// Derived from:
|
|
||||||
// https://al-e-shevelev.medium.com/how-to-reduce-scroll-sensitivity-of-viewpager2-widget-87797ad02414
|
|
||||||
try {
|
|
||||||
val recycler =
|
|
||||||
ViewPager2::class.java.getDeclaredField("mRecyclerView").run {
|
|
||||||
isAccessible = true
|
|
||||||
get(binding.homePager)
|
|
||||||
}
|
|
||||||
|
|
||||||
RecyclerView::class.java.getDeclaredField("mTouchSlop").apply {
|
|
||||||
isAccessible = true
|
|
||||||
|
|
||||||
val slop = get(recycler) as Int
|
|
||||||
set(recycler, slop * 3) // 3x seems to be the best fit here
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logE("Unable to reduce ViewPager sensitivity (likely an internal code change)")
|
|
||||||
e.logTraceOrThrow()
|
|
||||||
}
|
|
||||||
|
|
||||||
// We know that there will only be a fixed amount of tabs, so we manually set this
|
// We know that there will only be a fixed amount of tabs, so we manually set this
|
||||||
// limit to that. This also prevents the appbar lift state from being confused during
|
// limit to that. This also prevents the appbar lift state from being confused during
|
||||||
// page transitions.
|
// page transitions.
|
||||||
offscreenPageLimit = homeModel.tabs.size
|
offscreenPageLimit = homeModel.tabs.size
|
||||||
|
|
||||||
|
reduceSensitivity(3)
|
||||||
|
|
||||||
registerOnPageChangeCallback(
|
registerOnPageChangeCallback(
|
||||||
object : ViewPager2.OnPageChangeCallback() {
|
object : ViewPager2.OnPageChangeCallback() {
|
||||||
override fun onPageSelected(position: Int) =
|
override fun onPageSelected(position: Int) =
|
||||||
|
@ -171,89 +102,94 @@ class HomeFragment : Fragment() {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
// There is no way a fast scrolling event can continue across a re-create. Reset it.
|
homeModel.fastScrolling.observe(viewLifecycleOwner, ::updateFastScrolling)
|
||||||
homeModel.updateFastScrolling(false)
|
homeModel.currentTab.observe(viewLifecycleOwner) { tab -> updateCurrentTab(sortItem, tab) }
|
||||||
|
homeModel.recreateTabs.observe(viewLifecycleOwner, ::handleRecreateTabs)
|
||||||
|
|
||||||
musicModel.loaderResponse.observe(viewLifecycleOwner) { response ->
|
musicModel.loaderResponse.observe(viewLifecycleOwner, ::handleLoaderResponse)
|
||||||
// Handle the loader response.
|
detailModel.navToItem.observe(viewLifecycleOwner, ::handleNavigation)
|
||||||
when (response) {
|
}
|
||||||
is MusicStore.Response.Ok -> binding.homeFab.show()
|
|
||||||
|
|
||||||
// While loading or during an error, make sure we keep the shuffle fab hidden so
|
private fun onMenuClick(item: MenuItem) {
|
||||||
// that any kind of playback is impossible. PlaybackStateManager also relies on this
|
when (item.itemId) {
|
||||||
// invariant, so please don't change it.
|
R.id.action_search -> {
|
||||||
else -> binding.homeFab.hide()
|
logD("Navigating to search")
|
||||||
|
findNavController().navigate(HomeFragmentDirections.actionShowSearch())
|
||||||
|
}
|
||||||
|
R.id.action_settings -> {
|
||||||
|
logD("Navigating to settings")
|
||||||
|
parentFragment
|
||||||
|
?.parentFragment
|
||||||
|
?.findNavController()
|
||||||
|
?.navigate(MainFragmentDirections.actionShowSettings())
|
||||||
|
}
|
||||||
|
R.id.action_about -> {
|
||||||
|
logD("Navigating to about")
|
||||||
|
parentFragment
|
||||||
|
?.parentFragment
|
||||||
|
?.findNavController()
|
||||||
|
?.navigate(MainFragmentDirections.actionShowAbout())
|
||||||
|
}
|
||||||
|
R.id.submenu_sorting -> {}
|
||||||
|
R.id.option_sort_asc -> {
|
||||||
|
item.isChecked = !item.isChecked
|
||||||
|
homeModel.updateCurrentSort(
|
||||||
|
requireNotNull(
|
||||||
|
homeModel
|
||||||
|
.getSortForDisplay(homeModel.currentTab.value!!)
|
||||||
|
.ascending(item.isChecked)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorting option was selected, mark it as selected and update the mode
|
||||||
|
else -> {
|
||||||
|
item.isChecked = true
|
||||||
|
homeModel.updateCurrentSort(
|
||||||
|
requireNotNull(
|
||||||
|
homeModel
|
||||||
|
.getSortForDisplay(homeModel.currentTab.value!!)
|
||||||
|
.assignId(item.itemId)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
homeModel.fastScrolling.observe(viewLifecycleOwner) { scrolling ->
|
private fun updateFastScrolling(isFastScrolling: Boolean) {
|
||||||
// Make sure an update here doesn't mess up the FAB state when it comes to the
|
val binding = requireBinding()
|
||||||
// loader response.
|
|
||||||
if (musicModel.loaderResponse.value !is MusicStore.Response.Ok) {
|
|
||||||
return@observe
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrolling) {
|
// Make sure an update here doesn't mess up the FAB state when it comes to the
|
||||||
binding.homeFab.hide()
|
// loader response.
|
||||||
} else {
|
if (musicModel.loaderResponse.value !is MusicStore.Response.Ok) {
|
||||||
binding.homeFab.show()
|
return
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
homeModel.recreateTabs.observe(viewLifecycleOwner) { recreate ->
|
if (isFastScrolling) {
|
||||||
// notifyDataSetChanged is not practical for recreating here since it will cache
|
binding.homeFab.hide()
|
||||||
// the previous fragments. Just instantiate a whole new adapter.
|
} else {
|
||||||
if (recreate) {
|
binding.homeFab.show()
|
||||||
binding.homePager.currentItem = 0
|
}
|
||||||
binding.homePager.adapter = HomePagerAdapter()
|
}
|
||||||
homeModel.finishRecreateTabs()
|
|
||||||
|
private fun updateCurrentTab(sortItem: MenuItem, tab: DisplayMode) {
|
||||||
|
// Make sure that we update the scrolling view and allowed menu items whenever
|
||||||
|
// the tab changes.
|
||||||
|
val binding = requireBinding()
|
||||||
|
when (tab) {
|
||||||
|
DisplayMode.SHOW_SONGS -> {
|
||||||
|
updateSortMenu(sortItem, tab)
|
||||||
|
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_song_list
|
||||||
|
}
|
||||||
|
DisplayMode.SHOW_ALBUMS -> {
|
||||||
|
updateSortMenu(sortItem, tab) { id -> id != R.id.option_sort_album }
|
||||||
|
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_album_list
|
||||||
|
}
|
||||||
|
DisplayMode.SHOW_ARTISTS -> {
|
||||||
|
updateSortMenu(sortItem, tab) { id -> id == R.id.option_sort_asc }
|
||||||
|
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_artist_list
|
||||||
|
}
|
||||||
|
DisplayMode.SHOW_GENRES -> {
|
||||||
|
updateSortMenu(sortItem, tab) { id -> id == R.id.option_sort_asc }
|
||||||
|
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_genre_list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
homeModel.curTab.observe(viewLifecycleOwner) { t ->
|
|
||||||
val tab = requireNotNull(t)
|
|
||||||
|
|
||||||
// Make sure that we update the scrolling view and allowed menu items whenever
|
|
||||||
// the tab changes.
|
|
||||||
when (tab) {
|
|
||||||
DisplayMode.SHOW_SONGS -> updateSortMenu(sortItem, tab)
|
|
||||||
DisplayMode.SHOW_ALBUMS ->
|
|
||||||
updateSortMenu(sortItem, tab) { id -> id != R.id.option_sort_album }
|
|
||||||
DisplayMode.SHOW_ARTISTS ->
|
|
||||||
updateSortMenu(sortItem, tab) { id -> id == R.id.option_sort_asc }
|
|
||||||
DisplayMode.SHOW_GENRES ->
|
|
||||||
updateSortMenu(sortItem, tab) { id -> id == R.id.option_sort_asc }
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.homeAppbar.liftOnScrollTargetViewId = tab.viewId
|
|
||||||
}
|
|
||||||
|
|
||||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
|
||||||
// The AppBarLayout gets confused when we navigate too fast, wait for it to draw
|
|
||||||
// before we navigate.
|
|
||||||
// This is only here just in case a collapsing toolbar is re-added.
|
|
||||||
binding.homeAppbar.post {
|
|
||||||
when (item) {
|
|
||||||
is Song ->
|
|
||||||
findNavController()
|
|
||||||
.navigate(HomeFragmentDirections.actionShowAlbum(item.album.id))
|
|
||||||
is Album ->
|
|
||||||
findNavController()
|
|
||||||
.navigate(HomeFragmentDirections.actionShowAlbum(item.id))
|
|
||||||
is Artist ->
|
|
||||||
findNavController()
|
|
||||||
.navigate(HomeFragmentDirections.actionShowArtist(item.id))
|
|
||||||
is Genre ->
|
|
||||||
findNavController()
|
|
||||||
.navigate(HomeFragmentDirections.actionShowGenre(item.id))
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logD("Fragment Created")
|
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSortMenu(
|
private fun updateSortMenu(
|
||||||
|
@ -276,14 +212,72 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val DisplayMode.viewId: Int
|
private fun handleRecreateTabs(recreate: Boolean) {
|
||||||
get() =
|
if (recreate) {
|
||||||
when (this) {
|
requireBinding().homePager.recreate()
|
||||||
DisplayMode.SHOW_SONGS -> R.id.home_song_list
|
homeModel.finishRecreateTabs()
|
||||||
DisplayMode.SHOW_ALBUMS -> R.id.home_album_list
|
}
|
||||||
DisplayMode.SHOW_ARTISTS -> R.id.home_artist_list
|
}
|
||||||
DisplayMode.SHOW_GENRES -> R.id.home_genre_list
|
|
||||||
|
private fun handleLoaderResponse(response: MusicStore.Response?) {
|
||||||
|
val binding = requireBinding()
|
||||||
|
when (response) {
|
||||||
|
is MusicStore.Response.Ok -> binding.homeFab.show()
|
||||||
|
|
||||||
|
// While loading or during an error, make sure we keep the shuffle fab hidden so
|
||||||
|
// that any kind of playback is impossible. PlaybackStateManager also relies on this
|
||||||
|
// invariant, so please don't change it.
|
||||||
|
else -> binding.homeFab.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleNavigation(item: Music?) {
|
||||||
|
// Note: You will want to add a post call to this if you want to re-introduce a collapsing
|
||||||
|
// toolbar.
|
||||||
|
when (item) {
|
||||||
|
is Song ->
|
||||||
|
findNavController().navigate(HomeFragmentDirections.actionShowAlbum(item.album.id))
|
||||||
|
is Album ->
|
||||||
|
findNavController().navigate(HomeFragmentDirections.actionShowAlbum(item.id))
|
||||||
|
is Artist ->
|
||||||
|
findNavController().navigate(HomeFragmentDirections.actionShowArtist(item.id))
|
||||||
|
is Genre ->
|
||||||
|
findNavController().navigate(HomeFragmentDirections.actionShowGenre(item.id))
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, ViewPager2's sensitivity is high enough to result in vertical scroll events being
|
||||||
|
* registered as horizontal scroll events. Reflect into the internal recyclerview and change the
|
||||||
|
* touch slope so that touch actions will act more as a scroll than as a swipe. Derived from:
|
||||||
|
* https://al-e-shevelev.medium.com/how-to-reduce-scroll-sensitivity-of-viewpager2-widget-87797ad02414
|
||||||
|
*/
|
||||||
|
private fun ViewPager2.reduceSensitivity(by: Int) {
|
||||||
|
try {
|
||||||
|
val recycler =
|
||||||
|
ViewPager2::class.java.getDeclaredField("mRecyclerView").run {
|
||||||
|
isAccessible = true
|
||||||
|
get(this@reduceSensitivity)
|
||||||
|
}
|
||||||
|
|
||||||
|
RecyclerView::class.java.getDeclaredField("mTouchSlop").apply {
|
||||||
|
isAccessible = true
|
||||||
|
|
||||||
|
val slop = get(recycler) as Int
|
||||||
|
set(recycler, slop * by)
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logE("Unable to reduce ViewPager sensitivity (likely an internal code change)")
|
||||||
|
e.logTraceOrThrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Forces the view to recreate all fragments contained within it. */
|
||||||
|
private fun ViewPager2.recreate() {
|
||||||
|
currentItem = 0
|
||||||
|
adapter = HomePagerAdapter()
|
||||||
|
}
|
||||||
|
|
||||||
private inner class HomePagerAdapter :
|
private inner class HomePagerAdapter :
|
||||||
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) {
|
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) {
|
||||||
|
|
|
@ -63,8 +63,8 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
private val visibleTabs: List<DisplayMode>
|
private val visibleTabs: List<DisplayMode>
|
||||||
get() = settingsManager.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
|
get() = settingsManager.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
|
||||||
|
|
||||||
private val mCurTab = MutableLiveData(tabs[0])
|
private val mCurrentTab = MutableLiveData(tabs[0])
|
||||||
val curTab: LiveData<DisplayMode> = mCurTab
|
val currentTab: LiveData<DisplayMode> = mCurrentTab
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marker to recreate all library tabs, usually initiated by a settings change. When this flag
|
* Marker to recreate all library tabs, usually initiated by a settings change. When this flag
|
||||||
|
@ -91,7 +91,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
/** Update the current tab based off of the new ViewPager position. */
|
/** Update the current tab based off of the new ViewPager position. */
|
||||||
fun updateCurrentTab(pos: Int) {
|
fun updateCurrentTab(pos: Int) {
|
||||||
logD("Updating current tab to ${tabs[pos]}")
|
logD("Updating current tab to ${tabs[pos]}")
|
||||||
mCurTab.value = tabs[pos]
|
mCurrentTab.value = tabs[pos]
|
||||||
}
|
}
|
||||||
|
|
||||||
fun finishRecreateTabs() {
|
fun finishRecreateTabs() {
|
||||||
|
@ -109,8 +109,8 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
|
|
||||||
/** Update the currently displayed item's [Sort]. */
|
/** Update the currently displayed item's [Sort]. */
|
||||||
fun updateCurrentSort(sort: Sort) {
|
fun updateCurrentSort(sort: Sort) {
|
||||||
logD("Updating ${mCurTab.value} sort to $sort")
|
logD("Updating ${mCurrentTab.value} sort to $sort")
|
||||||
when (mCurTab.value) {
|
when (mCurrentTab.value) {
|
||||||
DisplayMode.SHOW_SONGS -> {
|
DisplayMode.SHOW_SONGS -> {
|
||||||
settingsManager.libSongSort = sort
|
settingsManager.libSongSort = sort
|
||||||
mSongs.value = sort.songs(mSongs.value!!)
|
mSongs.value = sort.songs(mSongs.value!!)
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
package org.oxycblt.auxio.home.list
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
@ -37,27 +36,15 @@ import org.oxycblt.auxio.ui.sliceArticle
|
||||||
* @author
|
* @author
|
||||||
*/
|
*/
|
||||||
class AlbumListFragment : HomeListFragment() {
|
class AlbumListFragment : HomeListFragment() {
|
||||||
override fun onCreateView(
|
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||||
inflater: LayoutInflater,
|
val homeAdapter =
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val binding = FragmentHomeListBinding.inflate(layoutInflater)
|
|
||||||
|
|
||||||
// / --- UI SETUP ---
|
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
|
|
||||||
val adapter =
|
|
||||||
AlbumAdapter(
|
AlbumAdapter(
|
||||||
doOnClick = { album ->
|
doOnClick = { album ->
|
||||||
findNavController().navigate(HomeFragmentDirections.actionShowAlbum(album.id))
|
findNavController().navigate(HomeFragmentDirections.actionShowAlbum(album.id))
|
||||||
},
|
},
|
||||||
::newMenu)
|
::newMenu)
|
||||||
|
|
||||||
setupRecycler(R.id.home_album_list, binding, adapter, homeModel.albums)
|
setupRecycler(R.id.home_album_list, homeAdapter, homeModel.albums)
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val listPopupProvider: (Int) -> String
|
override val listPopupProvider: (Int) -> String
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
package org.oxycblt.auxio.home.list
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
@ -35,27 +34,15 @@ import org.oxycblt.auxio.ui.sliceArticle
|
||||||
* @author
|
* @author
|
||||||
*/
|
*/
|
||||||
class ArtistListFragment : HomeListFragment() {
|
class ArtistListFragment : HomeListFragment() {
|
||||||
override fun onCreateView(
|
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||||
inflater: LayoutInflater,
|
val homeAdapter =
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val binding = FragmentHomeListBinding.inflate(layoutInflater)
|
|
||||||
|
|
||||||
// / --- UI SETUP ---
|
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
|
|
||||||
val adapter =
|
|
||||||
ArtistAdapter(
|
ArtistAdapter(
|
||||||
doOnClick = { artist ->
|
doOnClick = { artist ->
|
||||||
findNavController().navigate(HomeFragmentDirections.actionShowArtist(artist.id))
|
findNavController().navigate(HomeFragmentDirections.actionShowArtist(artist.id))
|
||||||
},
|
},
|
||||||
::newMenu)
|
::newMenu)
|
||||||
|
|
||||||
setupRecycler(R.id.home_artist_list, binding, adapter, homeModel.artists)
|
setupRecycler(R.id.home_artist_list, homeAdapter, homeModel.artists)
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val listPopupProvider: (Int) -> String
|
override val listPopupProvider: (Int) -> String
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
package org.oxycblt.auxio.home.list
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
@ -35,27 +34,15 @@ import org.oxycblt.auxio.ui.sliceArticle
|
||||||
* @author
|
* @author
|
||||||
*/
|
*/
|
||||||
class GenreListFragment : HomeListFragment() {
|
class GenreListFragment : HomeListFragment() {
|
||||||
override fun onCreateView(
|
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||||
inflater: LayoutInflater,
|
val homeAdapter =
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val binding = FragmentHomeListBinding.inflate(layoutInflater)
|
|
||||||
|
|
||||||
// / --- UI SETUP ---
|
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
|
|
||||||
val adapter =
|
|
||||||
GenreAdapter(
|
GenreAdapter(
|
||||||
doOnClick = { Genre ->
|
doOnClick = { Genre ->
|
||||||
findNavController().navigate(HomeFragmentDirections.actionShowGenre(Genre.id))
|
findNavController().navigate(HomeFragmentDirections.actionShowGenre(Genre.id))
|
||||||
},
|
},
|
||||||
::newMenu)
|
::newMenu)
|
||||||
|
|
||||||
setupRecycler(R.id.home_genre_list, binding, adapter, homeModel.genres)
|
setupRecycler(R.id.home_genre_list, homeAdapter, homeModel.genres)
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val listPopupProvider: (Int) -> String
|
override val listPopupProvider: (Int) -> String
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.oxycblt.auxio.home.list
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.view.LayoutInflater
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
@ -27,36 +28,43 @@ import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.home.HomeViewModel
|
import org.oxycblt.auxio.home.HomeViewModel
|
||||||
import org.oxycblt.auxio.music.Item
|
import org.oxycblt.auxio.music.Item
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.applySpans
|
import org.oxycblt.auxio.util.applySpans
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Base [Fragment] implementing the base features shared across all list fragments in the home UI.
|
* A Base [Fragment] implementing the base features shared across all list fragments in the home UI.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
abstract class HomeListFragment : Fragment() {
|
abstract class HomeListFragment : ViewBindingFragment<FragmentHomeListBinding>() {
|
||||||
protected val homeModel: HomeViewModel by activityViewModels()
|
|
||||||
protected val playbackModel: PlaybackViewModel by activityViewModels()
|
|
||||||
|
|
||||||
/** The popup provider to use for the fast scroller view. */
|
/** The popup provider to use for the fast scroller view. */
|
||||||
abstract val listPopupProvider: (Int) -> String
|
abstract val listPopupProvider: (Int) -> String
|
||||||
|
|
||||||
|
protected val homeModel: HomeViewModel by activityViewModels()
|
||||||
|
protected val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
|
||||||
protected fun <T : Item, VH : RecyclerView.ViewHolder> setupRecycler(
|
protected fun <T : Item, VH : RecyclerView.ViewHolder> setupRecycler(
|
||||||
@IdRes uniqueId: Int,
|
@IdRes uniqueId: Int,
|
||||||
binding: FragmentHomeListBinding,
|
|
||||||
homeAdapter: HomeAdapter<T, VH>,
|
homeAdapter: HomeAdapter<T, VH>,
|
||||||
homeData: LiveData<List<T>>,
|
homeData: LiveData<List<T>>,
|
||||||
) {
|
) {
|
||||||
binding.homeRecycler.apply {
|
requireBinding().homeRecycler.apply {
|
||||||
id = uniqueId
|
id = uniqueId
|
||||||
adapter = homeAdapter
|
adapter = homeAdapter
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
applySpans()
|
applySpans()
|
||||||
|
|
||||||
popupProvider = listPopupProvider
|
popupProvider = listPopupProvider
|
||||||
onDragListener = { dragging -> homeModel.updateFastScrolling(dragging) }
|
onDragListener = homeModel::updateFastScrolling
|
||||||
}
|
}
|
||||||
|
|
||||||
homeData.observe(viewLifecycleOwner) { data -> homeAdapter.updateData(data) }
|
homeData.observe(viewLifecycleOwner, homeAdapter::updateData)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||||
|
FragmentHomeListBinding.inflate(inflater)
|
||||||
|
|
||||||
|
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
|
||||||
|
homeModel.updateFastScrolling(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class HomeAdapter<T : Item, VH : RecyclerView.ViewHolder> :
|
abstract class HomeAdapter<T : Item, VH : RecyclerView.ViewHolder> :
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
package org.oxycblt.auxio.home.list
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
@ -35,22 +34,9 @@ import org.oxycblt.auxio.ui.sliceArticle
|
||||||
* @author
|
* @author
|
||||||
*/
|
*/
|
||||||
class SongListFragment : HomeListFragment() {
|
class SongListFragment : HomeListFragment() {
|
||||||
override fun onCreateView(
|
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||||
inflater: LayoutInflater,
|
val homeAdapter = SongsAdapter(doOnClick = playbackModel::playSong, ::newMenu)
|
||||||
container: ViewGroup?,
|
setupRecycler(R.id.home_song_list, homeAdapter, homeModel.songs)
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val binding = FragmentHomeListBinding.inflate(layoutInflater)
|
|
||||||
|
|
||||||
// / --- UI SETUP ---
|
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
|
|
||||||
val adapter = SongsAdapter(doOnClick = { song -> playbackModel.playSong(song) }, ::newMenu)
|
|
||||||
|
|
||||||
setupRecycler(R.id.home_song_list, binding, adapter, homeModel.songs)
|
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val listPopupProvider: (Int) -> String
|
override val listPopupProvider: (Int) -> String
|
||||||
|
|
|
@ -19,15 +19,13 @@ package org.oxycblt.auxio.home.tabs
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.DialogTabsBinding
|
import org.oxycblt.auxio.databinding.DialogTabsBinding
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
import org.oxycblt.auxio.ui.LifecycleDialog
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,65 +33,11 @@ import org.oxycblt.auxio.util.logD
|
||||||
* serializes it's state instead of
|
* serializes it's state instead of
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class TabCustomizeDialog : LifecycleDialog() {
|
class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>() {
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
private var pendingTabs = settingsManager.libTabs
|
private var pendingTabs = settingsManager.libTabs
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateBinding(inflater: LayoutInflater) = DialogTabsBinding.inflate(inflater)
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val binding = DialogTabsBinding.inflate(inflater)
|
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
// Restore any pending tab configurations
|
|
||||||
val tabs = Tab.fromSequence(savedInstanceState.getInt(KEY_TABS))
|
|
||||||
if (tabs != null) {
|
|
||||||
pendingTabs = tabs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up adapter & drag callback
|
|
||||||
val callback = TabDragCallback { pendingTabs }
|
|
||||||
val helper = ItemTouchHelper(callback)
|
|
||||||
val tabAdapter =
|
|
||||||
TabAdapter(
|
|
||||||
helper,
|
|
||||||
getTabs = { pendingTabs },
|
|
||||||
onTabSwitch = { tab ->
|
|
||||||
// Don't find the specific tab [Which might be outdated due to the nature
|
|
||||||
// of how ViewHolders are bound], but instead simply look for the mode in
|
|
||||||
// the list of pending tabs and update that instead.
|
|
||||||
val index = pendingTabs.indexOfFirst { it.mode == tab.mode }
|
|
||||||
if (index != -1) {
|
|
||||||
val curTab = pendingTabs[index]
|
|
||||||
logD("Updating tab $curTab to $tab")
|
|
||||||
pendingTabs[index] =
|
|
||||||
when (curTab) {
|
|
||||||
is Tab.Visible -> Tab.Invisible(curTab.mode)
|
|
||||||
is Tab.Invisible -> Tab.Visible(curTab.mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)
|
|
||||||
.isEnabled = pendingTabs.filterIsInstance<Tab.Visible>().isNotEmpty()
|
|
||||||
})
|
|
||||||
|
|
||||||
callback.addTabAdapter(tabAdapter)
|
|
||||||
|
|
||||||
binding.tabRecycler.apply {
|
|
||||||
adapter = tabAdapter
|
|
||||||
helper.attachToRecyclerView(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
outState.putInt(KEY_TABS, Tab.toSequence(pendingTabs))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConfigDialog(builder: AlertDialog.Builder) {
|
override fun onConfigDialog(builder: AlertDialog.Builder) {
|
||||||
builder.setTitle(R.string.set_lib_tabs)
|
builder.setTitle(R.string.set_lib_tabs)
|
||||||
|
@ -107,6 +51,52 @@ class TabCustomizeDialog : LifecycleDialog() {
|
||||||
builder.setNegativeButton(android.R.string.cancel, null)
|
builder.setNegativeButton(android.R.string.cancel, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onBindingCreated(binding: DialogTabsBinding, savedInstanceState: Bundle?) {
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
// Restore any pending tab configurations
|
||||||
|
val tabs = Tab.fromSequence(savedInstanceState.getInt(KEY_TABS))
|
||||||
|
if (tabs != null) {
|
||||||
|
pendingTabs = tabs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up adapter & drag callback
|
||||||
|
val callback = TabDragCallback { pendingTabs }
|
||||||
|
val helper = ItemTouchHelper(callback)
|
||||||
|
val tabAdapter = TabAdapter(helper, getTabs = { pendingTabs }, onTabSwitch = ::moveTabs)
|
||||||
|
|
||||||
|
callback.addTabAdapter(tabAdapter)
|
||||||
|
|
||||||
|
binding.tabRecycler.apply {
|
||||||
|
adapter = tabAdapter
|
||||||
|
helper.attachToRecyclerView(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putInt(KEY_TABS, Tab.toSequence(pendingTabs))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun moveTabs(tab: Tab) {
|
||||||
|
// Don't find the specific tab [Which might be outdated due to the nature
|
||||||
|
// of how ViewHolders are bound], but instead simply look for the mode in
|
||||||
|
// the list of pending tabs and update that instead.
|
||||||
|
val index = pendingTabs.indexOfFirst { it.mode == tab.mode }
|
||||||
|
if (index != -1) {
|
||||||
|
val curTab = pendingTabs[index]
|
||||||
|
logD("Updating tab $curTab to $tab")
|
||||||
|
pendingTabs[index] =
|
||||||
|
when (curTab) {
|
||||||
|
is Tab.Visible -> Tab.Invisible(curTab.mode)
|
||||||
|
is Tab.Invisible -> Tab.Visible(curTab.mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =
|
||||||
|
pendingTabs.filterIsInstance<Tab.Visible>().isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = BuildConfig.APPLICATION_ID + ".tag.TAB_CUSTOMIZE"
|
const val TAG = BuildConfig.APPLICATION_ID + ".tag.TAB_CUSTOMIZE"
|
||||||
const val KEY_TABS = BuildConfig.APPLICATION_ID + ".key.PENDING_TABS"
|
const val KEY_TABS = BuildConfig.APPLICATION_ID + ".key.PENDING_TABS"
|
||||||
|
|
|
@ -22,8 +22,6 @@ import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
@ -33,7 +31,7 @@ import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.DialogExcludedBinding
|
import org.oxycblt.auxio.databinding.DialogExcludedBinding
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.LifecycleDialog
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.hardRestart
|
import org.oxycblt.auxio.util.hardRestart
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
|
@ -42,27 +40,29 @@ import org.oxycblt.auxio.util.showToast
|
||||||
* Dialog that manages the currently excluded directories.
|
* Dialog that manages the currently excluded directories.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class ExcludedDialog : LifecycleDialog() {
|
class ExcludedDialog : ViewBindingDialogFragment<DialogExcludedBinding>() {
|
||||||
private val excludedModel: ExcludedViewModel by viewModels {
|
private val excludedModel: ExcludedViewModel by viewModels {
|
||||||
ExcludedViewModel.Factory(requireContext())
|
ExcludedViewModel.Factory(requireContext())
|
||||||
}
|
}
|
||||||
|
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateBinding(inflater: LayoutInflater) = DialogExcludedBinding.inflate(inflater)
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val binding = DialogExcludedBinding.inflate(inflater)
|
|
||||||
|
|
||||||
|
override fun onConfigDialog(builder: AlertDialog.Builder) {
|
||||||
|
builder.setTitle(R.string.set_excluded)
|
||||||
|
|
||||||
|
// Don't set the click listener here, we do some custom black magic in onCreateView instead.
|
||||||
|
builder.setNeutralButton(R.string.lbl_add, null)
|
||||||
|
builder.setPositiveButton(R.string.lbl_save, null)
|
||||||
|
builder.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindingCreated(binding: DialogExcludedBinding, savedInstanceState: Bundle?) {
|
||||||
val adapter = ExcludedEntryAdapter { path -> excludedModel.removePath(path) }
|
val adapter = ExcludedEntryAdapter { path -> excludedModel.removePath(path) }
|
||||||
|
|
||||||
val launcher =
|
val launcher =
|
||||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), ::addDocTreePath)
|
registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), ::addDocTreePath)
|
||||||
|
|
||||||
// --- UI SETUP ---
|
|
||||||
|
|
||||||
binding.excludedRecycler.adapter = adapter
|
binding.excludedRecycler.adapter = adapter
|
||||||
|
|
||||||
// Now that the dialog exists, we get the view manually when the dialog is shown
|
// Now that the dialog exists, we get the view manually when the dialog is shown
|
||||||
|
@ -90,23 +90,14 @@ class ExcludedDialog : LifecycleDialog() {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
excludedModel.paths.observe(viewLifecycleOwner) { paths ->
|
excludedModel.paths.observe(viewLifecycleOwner) { paths -> updatePaths(paths, adapter) }
|
||||||
adapter.submitList(paths)
|
|
||||||
binding.excludedEmpty.isVisible = paths.isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
logD("Dialog created")
|
logD("Dialog created")
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigDialog(builder: AlertDialog.Builder) {
|
private fun updatePaths(paths: MutableList<String>, adapter: ExcludedEntryAdapter) {
|
||||||
builder.setTitle(R.string.set_excluded)
|
adapter.submitList(paths)
|
||||||
|
requireBinding().excludedEmpty.isVisible = paths.isEmpty()
|
||||||
// Don't set the click listener here, we do some custom black magic in onCreateView instead.
|
|
||||||
builder.setNeutralButton(R.string.lbl_add, null)
|
|
||||||
builder.setPositiveButton(R.string.lbl_save, null)
|
|
||||||
builder.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addDocTreePath(uri: Uri?) {
|
private fun addDocTreePath(uri: Uri?) {
|
||||||
|
@ -147,17 +138,16 @@ class ExcludedDialog : LifecycleDialog() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getRootPath(): String {
|
||||||
|
return Environment.getExternalStorageDirectory().absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
private fun saveAndRestart() {
|
private fun saveAndRestart() {
|
||||||
excludedModel.save {
|
excludedModel.save {
|
||||||
playbackModel.savePlaybackState(requireContext()) { requireContext().hardRestart() }
|
playbackModel.savePlaybackState(requireContext()) { requireContext().hardRestart() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get *just* the root path, nothing else is really needed. */
|
|
||||||
private fun getRootPath(): String {
|
|
||||||
return Environment.getExternalStorageDirectory().absolutePath
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = BuildConfig.APPLICATION_ID + ".tag.EXCLUDED"
|
const val TAG = BuildConfig.APPLICATION_ID + ".tag.EXCLUDED"
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,8 @@ package org.oxycblt.auxio.playback
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
@ -33,22 +30,21 @@ import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding
|
||||||
import org.oxycblt.auxio.detail.DetailViewModel
|
import org.oxycblt.auxio.detail.DetailViewModel
|
||||||
import org.oxycblt.auxio.music.bindSongInfo
|
import org.oxycblt.auxio.music.bindSongInfo
|
||||||
import org.oxycblt.auxio.ui.BottomSheetLayout
|
import org.oxycblt.auxio.ui.BottomSheetLayout
|
||||||
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.getAttrColorSafe
|
import org.oxycblt.auxio.util.getAttrColorSafe
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
|
||||||
class PlaybackBarFragment : Fragment() {
|
class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||||
inflater: LayoutInflater,
|
FragmentPlaybackBarBinding.inflate(inflater)
|
||||||
container: ViewGroup?,
|
|
||||||
|
override fun onBindingCreated(
|
||||||
|
binding: FragmentPlaybackBarBinding,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
) {
|
||||||
val binding = FragmentPlaybackBarBinding.inflate(inflater)
|
|
||||||
|
|
||||||
// -- UI SETUP ---
|
|
||||||
|
|
||||||
binding.root.apply {
|
binding.root.apply {
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
// This is a dumb and fragile hack but this fragment isn't part of the navigation
|
// This is a dumb and fragile hack but this fragment isn't part of the navigation
|
||||||
|
@ -115,11 +111,9 @@ class PlaybackBarFragment : Fragment() {
|
||||||
binding.playbackPlayPause.isActivated = isPlaying
|
binding.playbackPlayPause.isActivated = isPlaying
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.playbackProgressBar.progress = playbackModel.seconds.value!!.toInt()
|
binding.playbackProgressBar.progress = playbackModel.positionSeconds.value!!.toInt()
|
||||||
playbackModel.seconds.observe(viewLifecycleOwner) { position ->
|
playbackModel.positionSeconds.observe(viewLifecycleOwner) { position ->
|
||||||
binding.playbackProgressBar.progress = position.toInt()
|
binding.playbackProgressBar.progress = position.toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,8 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.coil.bindAlbumCover
|
import org.oxycblt.auxio.coil.bindAlbumCover
|
||||||
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
|
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
|
||||||
import org.oxycblt.auxio.detail.DetailViewModel
|
import org.oxycblt.auxio.detail.DetailViewModel
|
||||||
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.toDuration
|
import org.oxycblt.auxio.music.toDuration
|
||||||
import org.oxycblt.auxio.playback.state.LoopMode
|
import org.oxycblt.auxio.playback.state.LoopMode
|
||||||
import org.oxycblt.auxio.ui.BottomSheetLayout
|
import org.oxycblt.auxio.ui.BottomSheetLayout
|
||||||
|
@ -55,24 +57,22 @@ class PlaybackPanelFragment :
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateBinding(inflater: LayoutInflater): FragmentPlaybackPanelBinding {
|
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||||
return FragmentPlaybackPanelBinding.inflate(inflater)
|
FragmentPlaybackPanelBinding.inflate(inflater)
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindingCreated(
|
override fun onBindingCreated(
|
||||||
binding: FragmentPlaybackPanelBinding,
|
binding: FragmentPlaybackPanelBinding,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
) {
|
) {
|
||||||
val queueItem: MenuItem
|
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
|
|
||||||
binding.root.setOnApplyWindowInsetsListener { _, insets ->
|
binding.root.setOnApplyWindowInsetsListener { _, insets ->
|
||||||
val bars = insets.systemBarInsetsCompat
|
val bars = insets.systemBarInsetsCompat
|
||||||
binding.root.updatePadding(top = bars.top, bottom = bars.bottom)
|
binding.root.updatePadding(top = bars.top, bottom = bars.bottom)
|
||||||
insets
|
insets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val queueItem: MenuItem
|
||||||
|
|
||||||
binding.playbackToolbar.apply {
|
binding.playbackToolbar.apply {
|
||||||
setNavigationOnClickListener { navigateUp() }
|
setNavigationOnClickListener { navigateUp() }
|
||||||
|
|
||||||
|
@ -114,7 +114,6 @@ class PlaybackPanelFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.playbackLoop.setOnClickListener { playbackModel.incrementLoopStatus() }
|
binding.playbackLoop.setOnClickListener { playbackModel.incrementLoopStatus() }
|
||||||
|
|
||||||
binding.playbackSkipPrev.setOnClickListener { playbackModel.skipPrev() }
|
binding.playbackSkipPrev.setOnClickListener { playbackModel.skipPrev() }
|
||||||
|
|
||||||
binding.playbackPlayPause.apply {
|
binding.playbackPlayPause.apply {
|
||||||
|
@ -124,69 +123,21 @@ class PlaybackPanelFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.playbackSkipNext.setOnClickListener { playbackModel.skipNext() }
|
binding.playbackSkipNext.setOnClickListener { playbackModel.skipNext() }
|
||||||
|
|
||||||
binding.playbackShuffle.setOnClickListener { playbackModel.invertShuffleStatus() }
|
binding.playbackShuffle.setOnClickListener { playbackModel.invertShuffleStatus() }
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP --
|
// --- VIEWMODEL SETUP --
|
||||||
|
|
||||||
playbackModel.song.observe(viewLifecycleOwner) { song ->
|
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
||||||
if (song != null) {
|
playbackModel.parent.observe(viewLifecycleOwner, ::updateParent)
|
||||||
logD("Updating song display to ${song.rawName}")
|
playbackModel.positionSeconds.observe(viewLifecycleOwner, ::updatePosition)
|
||||||
binding.playbackCover.bindAlbumCover(song)
|
playbackModel.loopMode.observe(viewLifecycleOwner, ::updateLoop)
|
||||||
binding.playbackSong.text = song.resolvedName
|
playbackModel.isPlaying.observe(viewLifecycleOwner, ::updatePlayPause)
|
||||||
binding.playbackArtist.text = song.resolvedArtistName
|
playbackModel.isShuffling.observe(viewLifecycleOwner, ::updateShuffle)
|
||||||
binding.playbackAlbum.text = song.resolvedAlbumName
|
|
||||||
|
|
||||||
// Normally if a song had a duration
|
playbackModel.nextUp.observe(viewLifecycleOwner) { nextUp ->
|
||||||
val seconds = song.seconds
|
|
||||||
binding.playbackDuration.text = seconds.toDuration(false)
|
|
||||||
binding.playbackSeekBar.apply {
|
|
||||||
valueTo = max(seconds, 1L).toFloat()
|
|
||||||
isEnabled = seconds > 0L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
playbackModel.parent.observe(viewLifecycleOwner) { parent ->
|
|
||||||
binding.playbackToolbar.subtitle =
|
|
||||||
parent?.resolvedName ?: getString(R.string.lbl_all_songs)
|
|
||||||
}
|
|
||||||
|
|
||||||
playbackModel.isShuffling.observe(viewLifecycleOwner) { isShuffling ->
|
|
||||||
binding.playbackShuffle.isActivated = isShuffling
|
|
||||||
}
|
|
||||||
|
|
||||||
playbackModel.loopMode.observe(viewLifecycleOwner) { loopMode ->
|
|
||||||
val resId =
|
|
||||||
when (loopMode) {
|
|
||||||
LoopMode.NONE, null -> R.drawable.ic_loop
|
|
||||||
LoopMode.ALL -> R.drawable.ic_loop_on
|
|
||||||
LoopMode.TRACK -> R.drawable.ic_loop_one
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.playbackLoop.apply {
|
|
||||||
isActivated = loopMode != LoopMode.NONE
|
|
||||||
setImageResource(resId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
playbackModel.seconds.observe(viewLifecycleOwner) { pos ->
|
|
||||||
// Don't update the progress while we are seeking, that will make the SeekBar jump
|
|
||||||
// around.
|
|
||||||
if (!binding.playbackSeconds.isActivated) {
|
|
||||||
binding.playbackSeekBar.value = pos.toFloat()
|
|
||||||
binding.playbackSeconds.text = pos.toDuration(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
playbackModel.nextUp.observe(viewLifecycleOwner) {
|
|
||||||
// The queue icon uses a selector that will automatically tint the icon as active or
|
// The queue icon uses a selector that will automatically tint the icon as active or
|
||||||
// inactive. We just need to set the flag.
|
// inactive. We just need to set the flag.
|
||||||
queueItem.isEnabled = playbackModel.nextUp.value!!.isNotEmpty()
|
queueItem.isEnabled = nextUp.isNotEmpty()
|
||||||
}
|
|
||||||
|
|
||||||
playbackModel.isPlaying.observe(viewLifecycleOwner) { isPlaying ->
|
|
||||||
binding.playbackPlayPause.isActivated = isPlaying
|
|
||||||
}
|
}
|
||||||
|
|
||||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
||||||
|
@ -205,20 +156,68 @@ class PlaybackPanelFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartTrackingTouch(slider: Slider) {
|
override fun onStartTrackingTouch(slider: Slider) {
|
||||||
requireBinding().playbackSeconds.isActivated = true
|
requireBinding().playbackPosition.isActivated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStopTrackingTouch(slider: Slider) {
|
override fun onStopTrackingTouch(slider: Slider) {
|
||||||
requireBinding().playbackSeconds.isActivated = false
|
requireBinding().playbackPosition.isActivated = false
|
||||||
playbackModel.setPosition(slider.value.toLong())
|
playbackModel.setPosition(slider.value.toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
||||||
if (fromUser) {
|
if (fromUser) {
|
||||||
requireBinding().playbackSeconds.text = value.toLong().toDuration(true)
|
requireBinding().playbackPosition.text = value.toLong().toDuration(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateSong(song: Song?) {
|
||||||
|
if (song == null) return
|
||||||
|
|
||||||
|
val binding = requireBinding()
|
||||||
|
binding.playbackCover.bindAlbumCover(song)
|
||||||
|
binding.playbackSong.text = song.resolvedName
|
||||||
|
binding.playbackArtist.text = song.resolvedArtistName
|
||||||
|
binding.playbackAlbum.text = song.resolvedAlbumName
|
||||||
|
|
||||||
|
// Normally if a song had a duration
|
||||||
|
val seconds = song.seconds
|
||||||
|
binding.playbackDuration.text = seconds.toDuration(false)
|
||||||
|
binding.playbackSeekBar.apply {
|
||||||
|
valueTo = max(seconds, 1L).toFloat()
|
||||||
|
isEnabled = seconds > 0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateParent(parent: MusicParent?) {
|
||||||
|
requireBinding().playbackToolbar.subtitle =
|
||||||
|
parent?.resolvedName ?: getString(R.string.lbl_all_songs)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePosition(position: Long) {
|
||||||
|
// Don't update the progress while we are seeking, that will make the SeekBar jump
|
||||||
|
// around.
|
||||||
|
val binding = requireBinding()
|
||||||
|
if (!binding.playbackPosition.isActivated) {
|
||||||
|
binding.playbackSeekBar.value = position.toFloat()
|
||||||
|
binding.playbackPosition.text = position.toDuration(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateLoop(loopMode: LoopMode) {
|
||||||
|
requireBinding().playbackLoop.apply {
|
||||||
|
isActivated = loopMode != LoopMode.NONE
|
||||||
|
setImageResource(loopMode.icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePlayPause(isPlaying: Boolean) {
|
||||||
|
requireBinding().playbackPlayPause.isActivated = isPlaying
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateShuffle(isShuffling: Boolean) {
|
||||||
|
requireBinding().playbackShuffle.isActivated = isShuffling
|
||||||
|
}
|
||||||
|
|
||||||
private fun navigateUp() {
|
private fun navigateUp() {
|
||||||
// This is a dumb and fragile hack but this fragment isn't part of the navigation stack
|
// This is a dumb and fragile hack but this fragment isn't part of the navigation stack
|
||||||
// so we can't really do much
|
// so we can't really do much
|
||||||
|
|
|
@ -58,7 +58,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
private val mIsPlaying = MutableLiveData(false)
|
private val mIsPlaying = MutableLiveData(false)
|
||||||
private val mIsShuffling = MutableLiveData(false)
|
private val mIsShuffling = MutableLiveData(false)
|
||||||
private val mLoopMode = MutableLiveData(LoopMode.NONE)
|
private val mLoopMode = MutableLiveData(LoopMode.NONE)
|
||||||
private val mSeconds = MutableLiveData(0L)
|
private val mPositionSeconds = MutableLiveData(0L)
|
||||||
|
|
||||||
// Queue
|
// Queue
|
||||||
private val mNextUp = MutableLiveData(listOf<Song>())
|
private val mNextUp = MutableLiveData(listOf<Song>())
|
||||||
|
@ -82,8 +82,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
val loopMode: LiveData<LoopMode>
|
val loopMode: LiveData<LoopMode>
|
||||||
get() = mLoopMode
|
get() = mLoopMode
|
||||||
/** The current playback position, in seconds */
|
/** The current playback position, in seconds */
|
||||||
val seconds: LiveData<Long>
|
val positionSeconds: LiveData<Long>
|
||||||
get() = mSeconds
|
get() = mPositionSeconds
|
||||||
|
|
||||||
/** The queue, without the previous items. */
|
/** The queue, without the previous items. */
|
||||||
val nextUp: LiveData<List<Song>>
|
val nextUp: LiveData<List<Song>>
|
||||||
|
@ -336,7 +336,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPositionUpdate(position: Long) {
|
override fun onPositionUpdate(position: Long) {
|
||||||
mSeconds.value = position / 1000
|
mPositionSeconds.value = position / 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQueueUpdate(queue: List<Song>, index: Int) {
|
override fun onQueueUpdate(queue: List<Song>, index: Int) {
|
||||||
|
|
|
@ -19,40 +19,32 @@ package org.oxycblt.auxio.playback.queue
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Fragment] that shows the queue and enables editing as well.
|
* A [Fragment] that shows the queue and enables editing as well.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class QueueFragment : Fragment() {
|
class QueueFragment : ViewBindingFragment<FragmentQueueBinding>() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
private var lastShuffle: Boolean? = null
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentQueueBinding.inflate(inflater)
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
override fun onBindingCreated(binding: FragmentQueueBinding, savedInstanceState: Bundle?) {
|
||||||
savedInstanceState: Bundle?
|
// TODO: Merge ItemTouchHelper with QueueAdapter
|
||||||
): View {
|
|
||||||
val binding = FragmentQueueBinding.inflate(inflater)
|
|
||||||
val callback = QueueDragCallback(playbackModel)
|
val callback = QueueDragCallback(playbackModel)
|
||||||
val helper = ItemTouchHelper(callback)
|
val helper = ItemTouchHelper(callback)
|
||||||
val queueAdapter = QueueAdapter(helper)
|
val queueAdapter = QueueAdapter(helper)
|
||||||
callback.addQueueAdapter(queueAdapter)
|
callback.addQueueAdapter(queueAdapter)
|
||||||
|
|
||||||
var lastShuffle = playbackModel.isShuffling.value
|
|
||||||
|
|
||||||
// --- UI SETUP ---
|
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
|
|
||||||
binding.queueToolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
binding.queueToolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
|
|
||||||
binding.queueRecycler.apply {
|
binding.queueRecycler.apply {
|
||||||
|
@ -63,15 +55,7 @@ class QueueFragment : Fragment() {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ----
|
// --- VIEWMODEL SETUP ----
|
||||||
|
|
||||||
playbackModel.nextUp.observe(viewLifecycleOwner) { queue ->
|
lastShuffle = playbackModel.isShuffling.value
|
||||||
if (queue.isEmpty()) {
|
|
||||||
findNavController().navigateUp()
|
|
||||||
return@observe
|
|
||||||
}
|
|
||||||
|
|
||||||
queueAdapter.submitList(queue.toMutableList())
|
|
||||||
}
|
|
||||||
|
|
||||||
playbackModel.isShuffling.observe(viewLifecycleOwner) { isShuffling ->
|
playbackModel.isShuffling.observe(viewLifecycleOwner) { isShuffling ->
|
||||||
// Try to prevent the queue adapter from going spastic during reshuffle events
|
// Try to prevent the queue adapter from going spastic during reshuffle events
|
||||||
// by just scrolling back to the top.
|
// by just scrolling back to the top.
|
||||||
|
@ -82,6 +66,13 @@ class QueueFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return binding.root
|
playbackModel.nextUp.observe(viewLifecycleOwner) { queue ->
|
||||||
|
if (queue.isEmpty()) {
|
||||||
|
findNavController().navigateUp()
|
||||||
|
return@observe
|
||||||
|
}
|
||||||
|
|
||||||
|
queueAdapter.submitList(queue.toMutableList())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.oxycblt.auxio.playback.state
|
package org.oxycblt.auxio.playback.state
|
||||||
|
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum that determines the playback repeat mode.
|
* Enum that determines the playback repeat mode.
|
||||||
|
@ -37,6 +38,14 @@ enum class LoopMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val icon: Int
|
||||||
|
get() =
|
||||||
|
when (this) {
|
||||||
|
NONE -> R.drawable.ic_loop
|
||||||
|
ALL -> R.drawable.ic_loop_on
|
||||||
|
TRACK -> R.drawable.ic_loop_one
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the LoopMode to an int constant that is saved in PlaybackStateDatabase
|
* Convert the LoopMode to an int constant that is saved in PlaybackStateDatabase
|
||||||
* @return The int constant for this mode
|
* @return The int constant for this mode
|
||||||
|
|
|
@ -72,6 +72,9 @@ import org.oxycblt.auxio.widgets.WidgetProvider
|
||||||
* This service relies on [PlaybackStateManager.Callback] and [SettingsManager.Callback], so
|
* This service relies on [PlaybackStateManager.Callback] and [SettingsManager.Callback], so
|
||||||
* therefore there's no need to bind to it to deliver commands.
|
* therefore there's no need to bind to it to deliver commands.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
|
*
|
||||||
|
* TODO: Move all external exposal from passing around PlaybackStateManager to passing around the
|
||||||
|
* MediaMetadata instance. Generally makes it easier to encapsulate this class.
|
||||||
*/
|
*/
|
||||||
class PlaybackService :
|
class PlaybackService :
|
||||||
Service(), Player.Listener, PlaybackStateManager.Callback, SettingsManager.Callback {
|
Service(), Player.Listener, PlaybackStateManager.Callback, SettingsManager.Callback {
|
||||||
|
@ -199,6 +202,8 @@ class PlaybackService :
|
||||||
|
|
||||||
// The service coroutines last job is to save the state to the DB, before terminating itself
|
// The service coroutines last job is to save the state to the DB, before terminating itself
|
||||||
// FIXME: This is a terrible idea, move this to when the user closes the notification
|
// FIXME: This is a terrible idea, move this to when the user closes the notification
|
||||||
|
// FIXME: Why not also encourage the user to disable battery optimizations while were
|
||||||
|
// at it? Would help prevent state saving issues to an extent.
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
playbackManager.saveStateToDatabase(this@PlaybackService)
|
playbackManager.saveStateToDatabase(this@PlaybackService)
|
||||||
serviceJob.cancel()
|
serviceJob.cancel()
|
||||||
|
|
|
@ -19,9 +19,8 @@ package org.oxycblt.auxio.search
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.postDelayed
|
import androidx.core.view.postDelayed
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
@ -35,10 +34,11 @@ import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Header
|
import org.oxycblt.auxio.music.Header
|
||||||
|
import org.oxycblt.auxio.music.Item
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
import org.oxycblt.auxio.util.applySpans
|
import org.oxycblt.auxio.util.applySpans
|
||||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||||
|
@ -48,41 +48,26 @@ import org.oxycblt.auxio.util.logD
|
||||||
* A [Fragment] that allows for the searching of the entire music library.
|
* A [Fragment] that allows for the searching of the entire music library.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class SearchFragment : Fragment() {
|
class SearchFragment : ViewBindingFragment<FragmentSearchBinding>() {
|
||||||
// SearchViewModel is only scoped to this Fragment
|
// SearchViewModel is only scoped to this Fragment
|
||||||
private val searchModel: SearchViewModel by viewModels()
|
private val searchModel: SearchViewModel by viewModels()
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
||||||
private var launchedKeyboard = false
|
private var launchedKeyboard = false
|
||||||
private var mustScrollUp = false
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentSearchBinding.inflate(inflater)
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val binding = FragmentSearchBinding.inflate(inflater)
|
|
||||||
|
|
||||||
|
override fun onBindingCreated(binding: FragmentSearchBinding, savedInstanceState: Bundle?) {
|
||||||
val imm = requireContext().getSystemServiceSafe(InputMethodManager::class)
|
val imm = requireContext().getSystemServiceSafe(InputMethodManager::class)
|
||||||
|
|
||||||
val searchAdapter =
|
val searchAdapter =
|
||||||
SearchAdapter(doOnClick = { item -> onItemSelection(item, imm) }, ::newMenu)
|
SearchAdapter(doOnClick = { item -> onItemSelection(item, imm) }, ::newMenu)
|
||||||
|
|
||||||
// --- UI SETUP --
|
// --- UI SETUP --
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
|
|
||||||
binding.searchToolbar.apply {
|
binding.searchToolbar.apply {
|
||||||
val itemId =
|
menu.findItem(searchModel.filterMode?.itemId ?: R.id.option_filter_all).isChecked = true
|
||||||
when (searchModel.filterMode) {
|
|
||||||
DisplayMode.SHOW_SONGS -> R.id.option_filter_songs
|
|
||||||
DisplayMode.SHOW_ALBUMS -> R.id.option_filter_albums
|
|
||||||
DisplayMode.SHOW_ARTISTS -> R.id.option_filter_artists
|
|
||||||
DisplayMode.SHOW_GENRES -> R.id.option_filter_genres
|
|
||||||
null -> R.id.option_filter_all
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.findItem(itemId).isChecked = true
|
|
||||||
|
|
||||||
setNavigationOnClickListener {
|
setNavigationOnClickListener {
|
||||||
imm.hide()
|
imm.hide()
|
||||||
|
@ -102,7 +87,6 @@ class SearchFragment : Fragment() {
|
||||||
|
|
||||||
binding.searchEditText.apply {
|
binding.searchEditText.apply {
|
||||||
addTextChangedListener { text ->
|
addTextChangedListener { text ->
|
||||||
mustScrollUp = true
|
|
||||||
// Run the search with the updated text as the query
|
// Run the search with the updated text as the query
|
||||||
searchModel.search(text?.toString() ?: "")
|
searchModel.search(text?.toString() ?: "")
|
||||||
}
|
}
|
||||||
|
@ -118,51 +102,50 @@ class SearchFragment : Fragment() {
|
||||||
|
|
||||||
binding.searchRecycler.apply {
|
binding.searchRecycler.apply {
|
||||||
adapter = searchAdapter
|
adapter = searchAdapter
|
||||||
|
|
||||||
applySpans { pos -> searchAdapter.currentList[pos] is Header }
|
applySpans { pos -> searchAdapter.currentList[pos] is Header }
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
searchModel.searchResults.observe(viewLifecycleOwner) { results ->
|
searchModel.searchResults.observe(viewLifecycleOwner) { results ->
|
||||||
searchAdapter.submitList(results) {
|
updateResults(results, searchAdapter)
|
||||||
// I would make it so that the position is only scrolled back to the top when
|
|
||||||
// the query actually changes instead of one every re-creation event, but sadly
|
|
||||||
// that doesn't seem possible.
|
|
||||||
binding.searchRecycler.scrollToPosition(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (results.isEmpty()) {
|
|
||||||
binding.searchRecycler.visibility = View.INVISIBLE
|
|
||||||
} else {
|
|
||||||
binding.searchRecycler.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
||||||
findNavController()
|
handleNavigation(item)
|
||||||
.navigate(
|
|
||||||
when (item) {
|
|
||||||
is Song -> SearchFragmentDirections.actionShowAlbum(item.album.id)
|
|
||||||
is Album -> SearchFragmentDirections.actionShowAlbum(item.id)
|
|
||||||
is Artist -> SearchFragmentDirections.actionShowArtist(item.id)
|
|
||||||
else -> return@observe
|
|
||||||
})
|
|
||||||
|
|
||||||
imm.hide()
|
imm.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Fragment created")
|
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
searchModel.setNavigating(false)
|
searchModel.setNavigating(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateResults(results: List<Item>, searchAdapter: SearchAdapter) {
|
||||||
|
val binding = requireBinding()
|
||||||
|
|
||||||
|
searchAdapter.submitList(results) {
|
||||||
|
// I would make it so that the position is only scrolled back to the top when
|
||||||
|
// the query actually changes instead of once every re-creation event, but sadly
|
||||||
|
// that doesn't seem possible.
|
||||||
|
binding.searchRecycler.scrollToPosition(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.searchRecycler.isInvisible = results.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleNavigation(item: Music?) {
|
||||||
|
findNavController()
|
||||||
|
.navigate(
|
||||||
|
when (item) {
|
||||||
|
is Song -> SearchFragmentDirections.actionShowAlbum(item.album.id)
|
||||||
|
is Album -> SearchFragmentDirections.actionShowAlbum(item.id)
|
||||||
|
is Artist -> SearchFragmentDirections.actionShowArtist(item.id)
|
||||||
|
else -> return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private fun InputMethodManager.hide() {
|
private fun InputMethodManager.hide() {
|
||||||
hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
|
hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,11 +23,8 @@ import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
@ -35,6 +32,7 @@ import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentAboutBinding
|
import org.oxycblt.auxio.databinding.FragmentAboutBinding
|
||||||
import org.oxycblt.auxio.home.HomeViewModel
|
import org.oxycblt.auxio.home.HomeViewModel
|
||||||
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
@ -43,18 +41,14 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
* A [BottomSheetDialogFragment] that shows Auxio's about screen.
|
* A [BottomSheetDialogFragment] that shows Auxio's about screen.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class AboutFragment : Fragment() {
|
class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentAboutBinding.inflate(inflater)
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val binding = FragmentAboutBinding.inflate(layoutInflater)
|
|
||||||
|
|
||||||
binding.aboutContents.setOnApplyWindowInsetsListener { _, insets ->
|
override fun onBindingCreated(binding: FragmentAboutBinding, savedInstanceState: Bundle?) {
|
||||||
binding.aboutContents.updatePadding(bottom = insets.systemBarInsetsCompat.bottom)
|
binding.aboutContents.setOnApplyWindowInsetsListener { view, insets ->
|
||||||
|
view.updatePadding(bottom = insets.systemBarInsetsCompat.bottom)
|
||||||
insets
|
insets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,10 +62,6 @@ class AboutFragment : Fragment() {
|
||||||
homeModel.songs.observe(viewLifecycleOwner) { songs ->
|
homeModel.songs.observe(viewLifecycleOwner) { songs ->
|
||||||
binding.aboutSongCount.text = getString(R.string.fmt_songs_loaded, songs.size)
|
binding.aboutSongCount.text = getString(R.string.fmt_songs_loaded, songs.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Dialog created")
|
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Go through the process of opening a [link] in a browser. */
|
/** Go through the process of opening a [link] in a browser. */
|
||||||
|
@ -100,8 +90,7 @@ class AboutFragment : Fragment() {
|
||||||
requireContext()
|
requireContext()
|
||||||
.packageManager
|
.packageManager
|
||||||
.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
|
.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
?.activityInfo
|
?.run { activityInfo.packageName }
|
||||||
?.packageName
|
|
||||||
|
|
||||||
if (pkgName != null) {
|
if (pkgName != null) {
|
||||||
if (pkgName == "android") {
|
if (pkgName == "android") {
|
||||||
|
|
|
@ -19,30 +19,21 @@ package org.oxycblt.auxio.settings
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import org.oxycblt.auxio.databinding.FragmentSettingsBinding
|
import org.oxycblt.auxio.databinding.FragmentSettingsBinding
|
||||||
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A container [Fragment] for the settings menu.
|
* A container [Fragment] for the settings menu.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class SettingsFragment : Fragment() {
|
class SettingsFragment : ViewBindingFragment<FragmentSettingsBinding>() {
|
||||||
override fun onCreateView(
|
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||||
inflater: LayoutInflater,
|
FragmentSettingsBinding.inflate(inflater)
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val binding = FragmentSettingsBinding.inflate(inflater)
|
|
||||||
|
|
||||||
binding.settingsToolbar.apply {
|
|
||||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
override fun onBindingCreated(binding: FragmentSettingsBinding, savedInstanceState: Bundle?) {
|
||||||
|
binding.settingsToolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
binding.settingsAppbar.liftOnScrollTargetViewId = androidx.preference.R.id.recycler_view
|
binding.settingsAppbar.liftOnScrollTargetViewId = androidx.preference.R.id.recycler_view
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,12 +57,12 @@ class SettingsListFragment : PreferenceFragmentCompat() {
|
||||||
preferenceManager.onDisplayPreferenceDialogListener = this
|
preferenceManager.onDisplayPreferenceDialogListener = this
|
||||||
preferenceScreen.children.forEach(::recursivelyHandlePreference)
|
preferenceScreen.children.forEach(::recursivelyHandlePreference)
|
||||||
|
|
||||||
|
// Make the RecycleBiew edge-to-edge capable
|
||||||
view.findViewById<RecyclerView>(androidx.preference.R.id.recycler_view).apply {
|
view.findViewById<RecyclerView>(androidx.preference.R.id.recycler_view).apply {
|
||||||
clipToPadding = false
|
clipToPadding = false
|
||||||
|
|
||||||
setOnApplyWindowInsetsListener { _, insets ->
|
setOnApplyWindowInsetsListener { _, insets ->
|
||||||
updatePadding(bottom = insets.systemBarInsetsCompat.bottom)
|
updatePadding(bottom = insets.systemBarInsetsCompat.bottom)
|
||||||
|
|
||||||
insets
|
insets
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,15 +17,18 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.settings.pref
|
package org.oxycblt.auxio.settings.pref
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.ui.LifecycleDialog
|
|
||||||
|
|
||||||
/** The dialog shown whenever an [IntListPreference] is shown. */
|
/** The dialog shown whenever an [IntListPreference] is shown. */
|
||||||
class IntListPrefDialog : LifecycleDialog() {
|
class IntListPrefDialog : DialogFragment() {
|
||||||
override fun onConfigDialog(builder: AlertDialog.Builder) {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val builder = MaterialAlertDialogBuilder(requireActivity(), theme)
|
||||||
|
|
||||||
// Since we have to store the preference key as an argument, we have to find the
|
// Since we have to store the preference key as an argument, we have to find the
|
||||||
// preference we need to use manually.
|
// preference we need to use manually.
|
||||||
val pref =
|
val pref =
|
||||||
|
@ -41,6 +44,8 @@ class IntListPrefDialog : LifecycleDialog() {
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.setNegativeButton(android.R.string.cancel, null)
|
builder.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
|
||||||
|
return builder.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -380,6 +380,11 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) :
|
||||||
// We kind of do a reverse-measure to figure out how we should inset this view.
|
// We kind of do a reverse-measure to figure out how we should inset this view.
|
||||||
// Find how much space is lost by the panel and then combine that with the
|
// Find how much space is lost by the panel and then combine that with the
|
||||||
// bottom inset to find how much space we should apply.
|
// bottom inset to find how much space we should apply.
|
||||||
|
// There is a slight shortcoming to this. If the playback bar has a height of
|
||||||
|
// zero (usually due to delays with fragment inflation), then it is assumed to
|
||||||
|
// not apply any window insets at all, which results in scroll desynchronization on
|
||||||
|
// certain views. This is considered tolerable as the other options are to convert
|
||||||
|
// the playback fragments to views, which is not nice.
|
||||||
val bars = insets.systemBarInsetsCompat
|
val bars = insets.systemBarInsetsCompat
|
||||||
val consumedByPanel = computePanelTopPosition(panelOffset) - measuredHeight
|
val consumedByPanel = computePanelTopPosition(panelOffset) - measuredHeight
|
||||||
val adjustedBottomInset = (consumedByPanel + bars.bottom).coerceAtLeast(0)
|
val adjustedBottomInset = (consumedByPanel + bars.bottom).coerceAtLeast(0)
|
||||||
|
|
|
@ -50,6 +50,15 @@ enum class DisplayMode {
|
||||||
SHOW_GENRES -> R.drawable.ic_genre
|
SHOW_GENRES -> R.drawable.ic_genre
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val itemId: Int
|
||||||
|
get() =
|
||||||
|
when (this) {
|
||||||
|
SHOW_SONGS -> R.drawable.ic_song
|
||||||
|
SHOW_ALBUMS -> R.drawable.ic_album
|
||||||
|
SHOW_ARTISTS -> R.drawable.ic_artist
|
||||||
|
SHOW_GENRES -> R.drawable.ic_genre
|
||||||
|
}
|
||||||
|
|
||||||
val intCode: Int
|
val intCode: Int
|
||||||
get() =
|
get() =
|
||||||
when (this) {
|
when (this) {
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2021 Auxio Project
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.oxycblt.auxio.ui
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.app.AppCompatDialogFragment
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A wrapper around [DialogFragment] that allows the usage of the standard Auxio lifecycle override
|
|
||||||
* [onCreateView] and [onDestroyView], but with a proper dialog being created.
|
|
||||||
*/
|
|
||||||
abstract class LifecycleDialog : AppCompatDialogFragment() {
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
val builder = MaterialAlertDialogBuilder(requireActivity(), theme)
|
|
||||||
onConfigDialog(builder)
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
(requireDialog() as AlertDialog).setView(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun onConfigDialog(builder: AlertDialog.Builder) {}
|
|
||||||
}
|
|
|
@ -98,7 +98,7 @@ sealed class Sort(open val isAscending: Boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun ascending(newIsAscending: Boolean): Sort {
|
override fun ascending(newIsAscending: Boolean): Sort {
|
||||||
return ByName(isAscending)
|
return ByName(newIsAscending)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,21 +239,17 @@ sealed class Sort(open val isAscending: Boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
class NameComparator<T : Music> : Comparator<T> {
|
class NameComparator<T : Music> : Comparator<T> {
|
||||||
override fun compare(a: T?, b: T?): Int {
|
override fun compare(a: T, b: T): Int {
|
||||||
if (a == null && b != null) return -1 // -1 -> a < b
|
return a.resolvedName
|
||||||
if (a == null && b == null) return 0 // 0 -> 0 = b
|
|
||||||
if (a != null && b == null) return 1 // 1 -> a > b
|
|
||||||
|
|
||||||
return a!!.resolvedName
|
|
||||||
.sliceArticle()
|
.sliceArticle()
|
||||||
.compareTo(b!!.resolvedName.sliceArticle(), ignoreCase = true)
|
.compareTo(b.resolvedName.sliceArticle(), ignoreCase = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NullableComparator<T : Comparable<T>> : Comparator<T?> {
|
class NullableComparator<T : Comparable<T>> : Comparator<T?> {
|
||||||
override fun compare(a: T?, b: T?): Int {
|
override fun compare(a: T?, b: T?): Int {
|
||||||
if (a == null && b != null) return -1 // -1 -> a < b
|
if (a == null && b != null) return -1 // -1 -> a < b
|
||||||
if (a == null && b == null) return 0 // 0 -> 0 = b
|
if (a == null && b == null) return 0 // 0 -> a = b
|
||||||
if (a != null && b == null) return 1 // 1 -> a > b
|
if (a != null && b == null) return 1 // 1 -> a > b
|
||||||
return a!!.compareTo(b!!)
|
return a!!.compareTo(b!!)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() {
|
||||||
|
private var mBinding: T? = null
|
||||||
|
|
||||||
|
protected abstract fun onCreateBinding(inflater: LayoutInflater): T
|
||||||
|
protected open fun onBindingCreated(binding: T, savedInstanceState: Bundle?) {}
|
||||||
|
protected open fun onDestroyBinding(binding: T) {}
|
||||||
|
protected open fun onConfigDialog(builder: AlertDialog.Builder) {}
|
||||||
|
|
||||||
|
protected val binding: T?
|
||||||
|
get() = mBinding
|
||||||
|
|
||||||
|
protected fun requireBinding(): T {
|
||||||
|
return requireNotNull(mBinding) {
|
||||||
|
"ViewBinding was not available, as the fragment was not in a valid state"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View = onCreateBinding(inflater).also { mBinding = it }.root
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
return MaterialAlertDialogBuilder(requireActivity(), theme).run {
|
||||||
|
onConfigDialog(this)
|
||||||
|
create()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
onBindingCreated(requireBinding(), savedInstanceState)
|
||||||
|
(requireDialog() as AlertDialog).setView(view)
|
||||||
|
logD("Fragment created")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
onDestroyBinding(requireBinding())
|
||||||
|
mBinding = null
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,23 +23,35 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/** A fragment enabling ViewBinding inflation and usage across the fragment lifecycle. */
|
/** A fragment enabling ViewBinding inflation and usage across the fragment lifecycle. */
|
||||||
abstract class ViewBindingFragment<T : ViewBinding> : Fragment() {
|
abstract class ViewBindingFragment<T : ViewBinding> : Fragment() {
|
||||||
private var mBinding: T? = null
|
private var mBinding: T? = null
|
||||||
|
|
||||||
abstract fun onCreateBinding(inflater: LayoutInflater): T
|
protected abstract fun onCreateBinding(inflater: LayoutInflater): T
|
||||||
abstract fun onBindingCreated(binding: T, savedInstanceState: Bundle?)
|
protected open fun onBindingCreated(binding: T, savedInstanceState: Bundle?) {}
|
||||||
abstract fun onDestroyBinding(binding: T)
|
protected open fun onDestroyBinding(binding: T) {}
|
||||||
|
|
||||||
|
protected val binding: T?
|
||||||
|
get() = mBinding
|
||||||
|
|
||||||
|
protected fun requireBinding(): T {
|
||||||
|
return requireNotNull(mBinding) {
|
||||||
|
"ViewBinding was not available, as the fragment was not in a valid state"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View = onCreateBinding(inflater).also { mBinding = it }.root
|
||||||
val binding = onCreateBinding(inflater).also { mBinding = it }
|
|
||||||
onBindingCreated(binding, savedInstanceState)
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
return binding.root
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
onBindingCreated(requireBinding(), savedInstanceState)
|
||||||
|
logD("Fragment created")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
@ -47,13 +59,4 @@ abstract class ViewBindingFragment<T : ViewBinding> : Fragment() {
|
||||||
onDestroyBinding(requireBinding())
|
onDestroyBinding(requireBinding())
|
||||||
mBinding = null
|
mBinding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
protected val binding: T?
|
|
||||||
get() = mBinding
|
|
||||||
|
|
||||||
protected fun requireBinding(): T {
|
|
||||||
return requireNotNull(mBinding) {
|
|
||||||
"ViewBinding was not available, as the fragment is not in a valid state"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,6 @@ import android.graphics.Insets
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
|
@ -93,9 +92,6 @@ private fun isUnderImpl(
|
||||||
if (viewSize >= minTouchTargetSize) {
|
if (viewSize >= minTouchTargetSize) {
|
||||||
return position >= viewStart && position < viewEnd
|
return position >= viewStart && position < viewEnd
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("Auxio.ViewUtil", "isInTouchTarget: $minTouchTargetSize")
|
|
||||||
|
|
||||||
var touchTargetStart = viewStart - (minTouchTargetSize - viewSize) / 2
|
var touchTargetStart = viewStart - (minTouchTargetSize - viewSize) / 2
|
||||||
|
|
||||||
if (touchTargetStart < 0) {
|
if (touchTargetStart < 0) {
|
||||||
|
|
|
@ -98,14 +98,14 @@
|
||||||
app:thumbRadius="@dimen/slider_thumb_radius" />
|
app:thumbRadius="@dimen/slider_thumb_radius" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/playback_seconds"
|
android:id="@+id/playback_position"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
|
||||||
android:layout_marginTop="@dimen/spacing_small_inv"
|
android:layout_marginTop="@dimen/spacing_small_inv"
|
||||||
|
android:layout_marginStart="@dimen/spacing_medium"
|
||||||
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
|
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
|
||||||
android:textColor="@color/sel_accented_secondary"
|
android:textColor="@color/sel_accented_secondary"
|
||||||
app:layout_constraintStart_toStartOf="@+id/playback_seek_bar"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_seek_bar"
|
app:layout_constraintTop_toBottomOf="@+id/playback_seek_bar"
|
||||||
tools:text="11:38" />
|
tools:text="11:38" />
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@
|
||||||
android:layout_marginEnd="@dimen/spacing_medium"
|
android:layout_marginEnd="@dimen/spacing_medium"
|
||||||
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
|
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
app:layout_constraintEnd_toEndOf="@+id/playback_seek_bar"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_seek_bar"
|
app:layout_constraintTop_toBottomOf="@+id/playback_seek_bar"
|
||||||
tools:text="16:16" />
|
tools:text="16:16" />
|
||||||
|
|
||||||
|
@ -125,12 +125,12 @@
|
||||||
android:id="@+id/playback_loop"
|
android:id="@+id/playback_loop"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
|
||||||
android:contentDescription="@string/desc_change_loop"
|
android:contentDescription="@string/desc_change_loop"
|
||||||
android:src="@drawable/ic_loop"
|
android:src="@drawable/ic_loop"
|
||||||
app:hasIndicator="true"
|
app:hasIndicator="true"
|
||||||
|
android:layout_marginStart="@dimen/spacing_medium"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/playback_skip_prev"
|
app:layout_constraintBottom_toBottomOf="@+id/playback_skip_prev"
|
||||||
app:layout_constraintStart_toStartOf="@+id/playback_seek_bar"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@+id/playback_skip_prev" />
|
app:layout_constraintTop_toTopOf="@+id/playback_skip_prev" />
|
||||||
|
|
||||||
<org.oxycblt.auxio.playback.PlaybackButton
|
<org.oxycblt.auxio.playback.PlaybackButton
|
||||||
|
@ -138,7 +138,6 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:contentDescription="@string/desc_skip_prev"
|
android:contentDescription="@string/desc_skip_prev"
|
||||||
android:onClick="@{() -> playbackModel.skipPrev()}"
|
|
||||||
android:src="@drawable/ic_skip_prev"
|
android:src="@drawable/ic_skip_prev"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
|
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/playback_play_pause"
|
app:layout_constraintEnd_toStartOf="@+id/playback_play_pause"
|
||||||
|
@ -173,12 +172,12 @@
|
||||||
android:id="@+id/playback_shuffle"
|
android:id="@+id/playback_shuffle"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="@dimen/spacing_medium"
|
|
||||||
android:contentDescription="@string/desc_shuffle"
|
android:contentDescription="@string/desc_shuffle"
|
||||||
android:src="@drawable/ic_shuffle"
|
android:src="@drawable/ic_shuffle"
|
||||||
|
android:layout_marginEnd="@dimen/spacing_medium"
|
||||||
app:hasIndicator="true"
|
app:hasIndicator="true"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/playback_skip_next"
|
app:layout_constraintBottom_toBottomOf="@+id/playback_skip_next"
|
||||||
app:layout_constraintEnd_toEndOf="@+id/playback_seek_bar"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@+id/playback_skip_next"
|
app:layout_constraintTop_toTopOf="@+id/playback_skip_next"
|
||||||
app:tint="@color/sel_accented" />
|
app:tint="@color/sel_accented" />
|
||||||
|
|
||||||
|
|
|
@ -4,22 +4,6 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
tools:context=".playback.PlaybackPanelFragment">
|
tools:context=".playback.PlaybackPanelFragment">
|
||||||
|
|
||||||
<data>
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="song"
|
|
||||||
type="org.oxycblt.auxio.music.Song" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="playbackModel"
|
|
||||||
type="org.oxycblt.auxio.playback.PlaybackViewModel" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="detailModel"
|
|
||||||
type="org.oxycblt.auxio.detail.DetailViewModel" />
|
|
||||||
</data>
|
|
||||||
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/playback_layout"
|
android:id="@+id/playback_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -112,7 +96,7 @@
|
||||||
app:thumbRadius="@dimen/slider_thumb_radius" />
|
app:thumbRadius="@dimen/slider_thumb_radius" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/playback_seconds"
|
android:id="@+id/playback_position"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
android:layout_marginStart="@dimen/spacing_medium"
|
||||||
|
|
|
@ -84,7 +84,7 @@
|
||||||
app:thumbRadius="@dimen/slider_thumb_radius" />
|
app:thumbRadius="@dimen/slider_thumb_radius" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/playback_seconds"
|
android:id="@+id/playback_position"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
android:layout_marginStart="@dimen/spacing_medium"
|
||||||
|
|
|
@ -4,21 +4,6 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
tools:context=".playback.PlaybackPanelFragment">
|
tools:context=".playback.PlaybackPanelFragment">
|
||||||
|
|
||||||
<data>
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="song"
|
|
||||||
type="org.oxycblt.auxio.music.Song" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="playbackModel"
|
|
||||||
type="org.oxycblt.auxio.playback.PlaybackViewModel" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="detailModel"
|
|
||||||
type="org.oxycblt.auxio.detail.DetailViewModel" />
|
|
||||||
</data>
|
|
||||||
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/playback_layout"
|
android:id="@+id/playback_layout"
|
||||||
|
@ -38,8 +23,6 @@
|
||||||
android:id="@+id/playback_cover"
|
android:id="@+id/playback_cover"
|
||||||
style="@style/Widget.Auxio.Image.Full"
|
style="@style/Widget.Auxio.Image.Full"
|
||||||
android:layout_margin="@dimen/spacing_medium"
|
android:layout_margin="@dimen/spacing_medium"
|
||||||
android:contentDescription="@{@string/desc_album_cover(song.resolvedName)}"
|
|
||||||
app:albumArt="@{song}"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar"
|
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar"
|
||||||
|
@ -65,8 +48,6 @@
|
||||||
style="@style/Widget.Auxio.TextView.Primary"
|
style="@style/Widget.Auxio.TextView.Primary"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:onClick="@{() -> detailModel.navToItem(playbackModel.song)}"
|
|
||||||
android:text="@{song.resolvedName}"
|
|
||||||
tools:text="Song Name" />
|
tools:text="Song Name" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
@ -78,8 +59,6 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
android:layout_marginStart="@dimen/spacing_medium"
|
||||||
android:layout_marginEnd="@dimen/spacing_medium"
|
android:layout_marginEnd="@dimen/spacing_medium"
|
||||||
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album.artist)}"
|
|
||||||
android:text="@{song.resolvedArtistName}"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/playback_album"
|
app:layout_constraintBottom_toTopOf="@+id/playback_album"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
@ -94,8 +73,6 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
android:layout_marginStart="@dimen/spacing_medium"
|
||||||
android:layout_marginEnd="@dimen/spacing_medium"
|
android:layout_marginEnd="@dimen/spacing_medium"
|
||||||
android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album)}"
|
|
||||||
android:text="@{song.resolvedAlbumName}"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
|
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
@ -121,7 +98,7 @@
|
||||||
app:thumbRadius="@dimen/slider_thumb_radius" />
|
app:thumbRadius="@dimen/slider_thumb_radius" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/playback_seconds"
|
android:id="@+id/playback_position"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
android:layout_marginStart="@dimen/spacing_medium"
|
||||||
|
@ -151,7 +128,6 @@
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
android:layout_marginStart="@dimen/spacing_medium"
|
||||||
android:contentDescription="@string/desc_change_loop"
|
android:contentDescription="@string/desc_change_loop"
|
||||||
app:hasIndicator="true"
|
app:hasIndicator="true"
|
||||||
android:onClick="@{() -> playbackModel.incrementLoopStatus()}"
|
|
||||||
android:src="@drawable/ic_loop"
|
android:src="@drawable/ic_loop"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/playback_skip_prev"
|
app:layout_constraintBottom_toBottomOf="@+id/playback_skip_prev"
|
||||||
app:layout_constraintHorizontal_chainStyle="packed"
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
|
@ -163,7 +139,6 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:contentDescription="@string/desc_skip_prev"
|
android:contentDescription="@string/desc_skip_prev"
|
||||||
android:onClick="@{() -> playbackModel.skipPrev()}"
|
|
||||||
android:src="@drawable/ic_skip_prev"
|
android:src="@drawable/ic_skip_prev"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
|
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/playback_play_pause"
|
app:layout_constraintEnd_toStartOf="@+id/playback_play_pause"
|
||||||
|
@ -176,7 +151,6 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:contentDescription="@string/desc_play_pause"
|
android:contentDescription="@string/desc_play_pause"
|
||||||
android:onClick="@{() -> playbackModel.invertPlayingStatus()}"
|
|
||||||
android:src="@drawable/sel_playing_state"
|
android:src="@drawable/sel_playing_state"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="@+id/playback_seek_bar"
|
app:layout_constraintEnd_toEndOf="@+id/playback_seek_bar"
|
||||||
|
@ -190,7 +164,6 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:contentDescription="@string/desc_skip_next"
|
android:contentDescription="@string/desc_skip_next"
|
||||||
android:onClick="@{() -> playbackModel.skipNext()}"
|
|
||||||
android:src="@drawable/ic_skip_next"
|
android:src="@drawable/ic_skip_next"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
|
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/playback_shuffle"
|
app:layout_constraintEnd_toStartOf="@+id/playback_shuffle"
|
||||||
|
@ -204,7 +177,6 @@
|
||||||
android:layout_marginEnd="@dimen/spacing_medium"
|
android:layout_marginEnd="@dimen/spacing_medium"
|
||||||
android:contentDescription="@string/desc_shuffle"
|
android:contentDescription="@string/desc_shuffle"
|
||||||
app:hasIndicator="true"
|
app:hasIndicator="true"
|
||||||
android:onClick="@{() -> playbackModel.invertShuffleStatus()}"
|
|
||||||
android:src="@drawable/ic_shuffle"
|
android:src="@drawable/ic_shuffle"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/playback_skip_next"
|
app:layout_constraintBottom_toBottomOf="@+id/playback_skip_next"
|
||||||
app:layout_constraintEnd_toEndOf="@+id/playback_seek_bar"
|
app:layout_constraintEnd_toEndOf="@+id/playback_seek_bar"
|
||||||
|
|
|
@ -82,7 +82,7 @@
|
||||||
app:thumbRadius="@dimen/slider_thumb_radius" />
|
app:thumbRadius="@dimen/slider_thumb_radius" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/playback_seconds"
|
android:id="@+id/playback_position"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
android:layout_marginStart="@dimen/spacing_medium"
|
||||||
|
@ -97,11 +97,11 @@
|
||||||
android:id="@+id/playback_duration"
|
android:id="@+id/playback_duration"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="@dimen/spacing_medium"
|
|
||||||
android:layout_marginTop="@dimen/spacing_small_inv"
|
android:layout_marginTop="@dimen/spacing_small_inv"
|
||||||
|
android:layout_marginEnd="@dimen/spacing_medium"
|
||||||
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
|
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
app:layout_constraintEnd_toEndOf="@+id/playback_seek_bar"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_seek_bar"
|
app:layout_constraintTop_toBottomOf="@+id/playback_seek_bar"
|
||||||
tools:text="16:16" />
|
tools:text="16:16" />
|
||||||
|
|
||||||
|
@ -109,12 +109,12 @@
|
||||||
android:id="@+id/playback_loop"
|
android:id="@+id/playback_loop"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
|
||||||
android:contentDescription="@string/desc_change_loop"
|
android:contentDescription="@string/desc_change_loop"
|
||||||
android:src="@drawable/ic_loop"
|
android:src="@drawable/ic_loop"
|
||||||
app:hasIndicator="true"
|
app:hasIndicator="true"
|
||||||
|
android:layout_marginStart="@dimen/spacing_medium"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/playback_skip_prev"
|
app:layout_constraintBottom_toBottomOf="@+id/playback_skip_prev"
|
||||||
app:layout_constraintStart_toStartOf="@+id/playback_seek_bar"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@+id/playback_skip_prev" />
|
app:layout_constraintTop_toTopOf="@+id/playback_skip_prev" />
|
||||||
|
|
||||||
<org.oxycblt.auxio.playback.PlaybackButton
|
<org.oxycblt.auxio.playback.PlaybackButton
|
||||||
|
@ -137,17 +137,17 @@
|
||||||
android:contentDescription="@string/desc_play_pause"
|
android:contentDescription="@string/desc_play_pause"
|
||||||
android:src="@drawable/sel_playing_state"
|
android:src="@drawable/sel_playing_state"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="@+id/playback_seek_bar"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="@+id/playback_seek_bar"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
tools:src="@drawable/ic_play" />
|
tools:src="@drawable/ic_play" />
|
||||||
|
|
||||||
<org.oxycblt.auxio.playback.PlaybackButton
|
<org.oxycblt.auxio.playback.PlaybackButton
|
||||||
android:id="@+id/playback_skip_next"
|
android:id="@+id/playback_skip_next"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:minHeight="@dimen/size_btn_small"
|
|
||||||
android:minWidth="@dimen/size_btn_small"
|
|
||||||
android:contentDescription="@string/desc_skip_next"
|
android:contentDescription="@string/desc_skip_next"
|
||||||
|
android:minWidth="@dimen/size_btn_small"
|
||||||
|
android:minHeight="@dimen/size_btn_small"
|
||||||
android:src="@drawable/ic_skip_next"
|
android:src="@drawable/ic_skip_next"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
|
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/playback_shuffle"
|
app:layout_constraintEnd_toStartOf="@+id/playback_shuffle"
|
||||||
|
@ -158,14 +158,14 @@
|
||||||
android:id="@+id/playback_shuffle"
|
android:id="@+id/playback_shuffle"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:minHeight="@dimen/size_btn_small"
|
|
||||||
android:minWidth="@dimen/size_btn_small"
|
|
||||||
android:layout_marginEnd="@dimen/spacing_medium"
|
|
||||||
android:contentDescription="@string/desc_shuffle"
|
android:contentDescription="@string/desc_shuffle"
|
||||||
|
android:minWidth="@dimen/size_btn_small"
|
||||||
|
android:minHeight="@dimen/size_btn_small"
|
||||||
android:src="@drawable/ic_shuffle"
|
android:src="@drawable/ic_shuffle"
|
||||||
|
android:layout_marginEnd="@dimen/spacing_medium"
|
||||||
app:hasIndicator="true"
|
app:hasIndicator="true"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/playback_skip_next"
|
app:layout_constraintBottom_toBottomOf="@+id/playback_skip_next"
|
||||||
app:layout_constraintEnd_toEndOf="@+id/playback_seek_bar"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@+id/playback_skip_next"
|
app:layout_constraintTop_toTopOf="@+id/playback_skip_next"
|
||||||
app:tint="@color/sel_accented" />
|
app:tint="@color/sel_accented" />
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/playback_seconds"
|
android:id="@+id/playback_position"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
android:layout_marginStart="@dimen/spacing_medium"
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
<dimen name="fast_scroll_thumb_touch_target_size">16dp</dimen>
|
<dimen name="fast_scroll_thumb_touch_target_size">16dp</dimen>
|
||||||
|
|
||||||
<dimen name="slider_thumb_radius">6dp</dimen>
|
<dimen name="slider_thumb_radius">6dp</dimen>
|
||||||
<dimen name="slider_halo_radius">12dp</dimen>
|
<dimen name="slider_halo_radius">16dp</dimen>
|
||||||
|
|
||||||
<dimen name="recycler_fab_space_normal">88dp</dimen>
|
<dimen name="recycler_fab_space_normal">88dp</dimen>
|
||||||
<dimen name="recycler_fab_space_large">128dp</dimen>
|
<dimen name="recycler_fab_space_large">128dp</dimen>
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
<style name="Widget.Auxio.TextView.Primary.AppWidget" parent="Widget.Auxio.TextView.AppWidget">
|
<style name="Widget.Auxio.TextView.Primary.AppWidget" parent="Widget.Auxio.TextView.AppWidget">
|
||||||
<item name="android:textStyle">normal</item>
|
<item name="android:textStyle">normal</item>
|
||||||
<item name="android:textSize">@dimen/text_size_ext_title_mid_large</item>
|
<item name="android:textSize">@dimen/text_size_ext_title_mid_large</item>
|
||||||
|
<item name="android:letterSpacing">0</item>
|
||||||
<item name="android:textAppearance">@style/TextAppearance.Material3.TitleMedium</item>
|
<item name="android:textAppearance">@style/TextAppearance.Material3.TitleMedium</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue