diff --git a/AuxioTODO b/AuxioTODO index 47a74a41d..40b05a6cb 100644 --- a/AuxioTODO +++ b/AuxioTODO @@ -17,8 +17,8 @@ TODOs surrounded with !s are things I tried to do, but failed for reasons includ /songs/ +- Search when LibraryFragment isnt enabled - ? Sorting ? -- ? Search ? - ? Fast Scrolling ? /library/ @@ -28,9 +28,14 @@ TODOs surrounded with !s are things I tried to do, but failed for reasons includ - ? Add Nested Nav to Library ViewPager fragment [Hold fire on this until everything else is added, there could be sneaky bugs later on if you add it now] ? - ! Move Adapter functionality to ListAdapter [RecyclerView scrolls to middle/bottom when data is re-sorted] ! +/playback/ +- + +/other/ +- Highlight recycler items when they are being played + /bugs/ - Fix issue where fast navigations will cause the app to not display anything To be added: -/prefs/ -/playback/ \ No newline at end of file +/prefs/ \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 16a5472cf..b6a04720d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -47,9 +47,9 @@ dependencies { // --- SUPPORT --- // General - implementation 'androidx.core:core-ktx:1.3.1' - implementation 'androidx.activity:activity:1.2.0-alpha08' - implementation 'androidx.fragment:fragment:1.3.0-alpha08' + implementation 'androidx.core:core-ktx:1.3.2' + implementation 'androidx.activity:activity:1.2.0-beta01' + implementation 'androidx.fragment:fragment:1.3.0-beta01' // Layout implementation 'androidx.constraintlayout:constraintlayout:2.0.1' @@ -71,7 +71,7 @@ dependencies { implementation 'io.coil-kt:coil:0.13.0' // Material - implementation 'com.google.android.material:material:1.3.0-alpha02' + implementation 'com.google.android.material:material:1.3.0-alpha03' // Lint ktlint "com.pinterest:ktlint:0.37.2" diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index a0ce81261..84cb3026b 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -41,7 +41,7 @@ class MainFragment : Fragment() { val binding = FragmentMainBinding.inflate(inflater) // If musicModel was cleared while the app was closed [Likely due to Auxio being suspended - // in the background], then navigate back to loading to reload the music. + // in the background], then navigate back to LoadingFragment to reload the music. if (musicModel.response.value == null) { findNavController().navigate(MainFragmentDirections.actionReturnToLoading()) @@ -57,7 +57,7 @@ class MainFragment : Fragment() { // --- UI SETUP --- - binding.lifecycleOwner = viewLifecycleOwner + binding.lifecycleOwner = this binding.mainViewPager.adapter = PagerAdapter() // Link the ViewPager & Tab View diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index df671d046..e58c91503 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -13,6 +13,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentAlbumDetailBinding import org.oxycblt.auxio.detail.adapters.DetailSongAdapter import org.oxycblt.auxio.music.MusicViewModel +import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.recycler.ClickListener import org.oxycblt.auxio.theme.applyDivider import org.oxycblt.auxio.theme.disable @@ -21,6 +22,7 @@ class AlbumDetailFragment : Fragment() { private val args: AlbumDetailFragmentArgs by navArgs() private val detailModel: DetailViewModel by activityViewModels() + private val playbackModel: PlaybackViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, @@ -45,7 +47,7 @@ class AlbumDetailFragment : Fragment() { val songAdapter = DetailSongAdapter( ClickListener { - Log.d(this::class.simpleName, it.name) + playbackModel.updateSong(it) } ) diff --git a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt index 2d17cb16b..38c174d8d 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt @@ -17,13 +17,15 @@ import androidx.transition.TransitionManager import org.oxycblt.auxio.MainFragmentDirections import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentLibraryBinding -import org.oxycblt.auxio.library.recycler.LibraryAdapter -import org.oxycblt.auxio.library.recycler.SearchAdapter +import org.oxycblt.auxio.library.adapters.LibraryAdapter +import org.oxycblt.auxio.library.adapters.SearchAdapter import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.MusicViewModel +import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.recycler.ShowMode import org.oxycblt.auxio.theme.applyColor import org.oxycblt.auxio.theme.applyDivider @@ -36,6 +38,7 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener { } private val libraryModel: LibraryViewModel by activityViewModels() + private val playbackModel: PlaybackViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, @@ -173,6 +176,13 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener { } private fun navToItem(baseModel: BaseModel) { + // If the item is a song [That was selected through search], then update the playback + // to that song instead of doing any naviagation + if (baseModel is Song) { + playbackModel.updateSong(baseModel) + return + } + if (!libraryModel.isNavigating) { libraryModel.updateNavigationStatus(true) diff --git a/app/src/main/java/org/oxycblt/auxio/library/recycler/LibraryAdapter.kt b/app/src/main/java/org/oxycblt/auxio/library/adapters/LibraryAdapter.kt similarity index 94% rename from app/src/main/java/org/oxycblt/auxio/library/recycler/LibraryAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/library/adapters/LibraryAdapter.kt index 7c1807678..9bc0421a2 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/recycler/LibraryAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/adapters/LibraryAdapter.kt @@ -1,4 +1,4 @@ -package org.oxycblt.auxio.library.recycler +package org.oxycblt.auxio.library.adapters import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView @@ -13,7 +13,8 @@ import org.oxycblt.auxio.recycler.viewholders.ArtistViewHolder import org.oxycblt.auxio.recycler.viewholders.GenreViewHolder // A ListAdapter that can contain three different types of ViewHolders depending -// the showmode given. It cannot display multiple types of viewholders *at once*. +// the ShowMode given. +// It cannot display multiple ViewHolders *at once* however. That's what SearchAdapter is for. class LibraryAdapter( private val showMode: ShowMode, private val doOnClick: (BaseModel) -> Unit diff --git a/app/src/main/java/org/oxycblt/auxio/library/recycler/SearchAdapter.kt b/app/src/main/java/org/oxycblt/auxio/library/adapters/SearchAdapter.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/library/recycler/SearchAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/library/adapters/SearchAdapter.kt index bcf38301b..541a821bc 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/recycler/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/adapters/SearchAdapter.kt @@ -1,4 +1,4 @@ -package org.oxycblt.auxio.library.recycler +package org.oxycblt.auxio.library.adapters import android.view.ViewGroup import androidx.recyclerview.widget.ListAdapter diff --git a/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt b/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt index 5e234e40c..545399a31 100644 --- a/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt @@ -20,62 +20,44 @@ import org.oxycblt.auxio.music.processing.MusicLoaderResponse class LoadingFragment : Fragment(R.layout.fragment_loading) { - // LoadingFragment is [hopefully] going to be the first one to have to create musicModel, - // so pass a factory instance so that the model has access to the application resources. private val musicModel: MusicViewModel by activityViewModels { MusicViewModel.Factory(requireActivity().application) } - private lateinit var binding: FragmentLoadingBinding - private lateinit var permLauncher: ActivityResultLauncher - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentLoadingBinding.inflate(inflater) - - binding.lifecycleOwner = this - binding.musicModel = musicModel - - musicModel.response.observe( - viewLifecycleOwner, - { response -> - onMusicLoadResponse(response) - } - ) - - musicModel.doReload.observe( - viewLifecycleOwner, - { retry -> - onRetry(retry) - } - ) - - musicModel.doGrant.observe( - viewLifecycleOwner, - { grant -> - onGrant(grant) - } - ) + val binding = FragmentLoadingBinding.inflate(inflater) // Set up the permission launcher, as its disallowed outside of onCreate. - permLauncher = + val permLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { granted: Boolean -> // If its actually granted, restart the loading process again. if (granted) { - wipeViews() + wipeViews(binding) musicModel.reload() } } + // --- UI SETUP --- + + binding.lifecycleOwner = this + binding.musicModel = musicModel + + // --- VIEWMODEL SETUP --- + + musicModel.response.observe(viewLifecycleOwner) { onMusicLoadResponse(it, binding) } + musicModel.doReload.observe(viewLifecycleOwner) { onRetry(it, binding) } + musicModel.doGrant.observe(viewLifecycleOwner) { onGrant(it, permLauncher) } + // Force an error screen if the permissions are denied or the prompt needs to be shown. if (checkPerms()) { - onNoPerms() + onNoPerms(binding) } else { musicModel.go() } @@ -96,31 +78,31 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) { ) == PackageManager.PERMISSION_DENIED } - private fun onMusicLoadResponse(response: MusicLoaderResponse?) { + private fun onMusicLoadResponse( + response: MusicLoaderResponse?, + binding: FragmentLoadingBinding + ) { if (response == MusicLoaderResponse.DONE) { findNavController().navigate( LoadingFragmentDirections.actionToMain() ) } else { - binding.let { binding -> - binding.loadingErrorText.text = - if (response == MusicLoaderResponse.NO_MUSIC) - getString(R.string.error_no_music) - else - getString(R.string.error_music_load_failed) + binding.loadingErrorText.text = + if (response == MusicLoaderResponse.NO_MUSIC) + getString(R.string.error_no_music) + else + getString(R.string.error_music_load_failed) - // If the response wasn't a success, then show the specific error message - // depending on which error response was given, along with a retry button - binding.loadingBar.visibility = View.GONE - - binding.loadingErrorText.visibility = View.VISIBLE - binding.loadingErrorIcon.visibility = View.VISIBLE - binding.loadingRetryButton.visibility = View.VISIBLE - } + // If the response wasn't a success, then show the specific error message + // depending on which error response was given, along with a retry button + binding.loadingBar.visibility = View.GONE + binding.loadingErrorText.visibility = View.VISIBLE + binding.loadingErrorIcon.visibility = View.VISIBLE + binding.loadingRetryButton.visibility = View.VISIBLE } } - private fun onNoPerms() { + private fun onNoPerms(binding: FragmentLoadingBinding) { // If there are no perms, switch out the view elements as if an error screen was being // shown, but show the label that Auxio needs to read external storage to function, // along with a GRANT button @@ -133,15 +115,15 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) { binding.loadingErrorText.text = getString(R.string.error_no_perms) } - private fun onRetry(retry: Boolean) { + private fun onRetry(retry: Boolean, binding: FragmentLoadingBinding) { if (retry) { - wipeViews() + wipeViews(binding) musicModel.doneWithReload() } } - private fun onGrant(grant: Boolean) { + private fun onGrant(grant: Boolean, permLauncher: ActivityResultLauncher) { if (grant) { permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) @@ -150,7 +132,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) { } // Wipe views and switch back to the plain ProgressBar - private fun wipeViews() { + private fun wipeViews(binding: FragmentLoadingBinding) { binding.loadingBar.visibility = View.VISIBLE binding.loadingErrorText.visibility = View.GONE binding.loadingErrorIcon.visibility = View.GONE diff --git a/app/src/main/java/org/oxycblt/auxio/music/Models.kt b/app/src/main/java/org/oxycblt/auxio/music/Models.kt index c6deeb31a..d1b6861e9 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Models.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Models.kt @@ -13,9 +13,9 @@ sealed class BaseModel { data class Song( override val id: Long = -1, override var name: String, - val albumId: Long, - val track: Int, - val duration: Long, + val albumId: Long = -1, + val track: Int = -1, + val duration: Long = 0, ) : BaseModel() { lateinit var album: Album diff --git a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt new file mode 100644 index 000000000..d8142266a --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt @@ -0,0 +1,52 @@ +package org.oxycblt.auxio.playback + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding +import org.oxycblt.auxio.music.MusicViewModel + +class CompactPlaybackFragment : Fragment() { + private val musicModel: MusicViewModel by activityViewModels { + MusicViewModel.Factory(requireActivity().application) + } + + private val playbackModel: PlaybackViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val binding = FragmentCompactPlaybackBinding.inflate(inflater) + + binding.lifecycleOwner = this + + // Put a placeholder song in the binding & hide the playback fragment initially, + // as for some reason the attach event doesn't register anymore w/LiveData + binding.song = musicModel.songs.value!![0] + binding.root.visibility = View.GONE + + playbackModel.currentSong.observe(viewLifecycleOwner) { + if (it == null) { + Log.d(this::class.simpleName, "Hiding playback bar due to no song being played.") + + binding.root.visibility = View.GONE + } else { + Log.d(this::class.simpleName, "Updating song display to ${it.name}") + + binding.song = it + + binding.root.visibility = View.VISIBLE + } + } + + Log.d(this::class.simpleName, "Fragment Created") + + return binding.root + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt new file mode 100644 index 000000000..876d7236f --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -0,0 +1,15 @@ +package org.oxycblt.auxio.playback + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.oxycblt.auxio.music.Song + +class PlaybackViewModel : ViewModel() { + private val mCurrentSong = MutableLiveData() + val currentSong: LiveData get() = mCurrentSong + + fun updateSong(song: Song) { + mCurrentSong.value = song + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt index e8d5f3a4d..6514f2b20 100644 --- a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt @@ -9,15 +9,17 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import org.oxycblt.auxio.databinding.FragmentSongsBinding import org.oxycblt.auxio.music.MusicViewModel +import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.recycler.ClickListener import org.oxycblt.auxio.theme.applyDivider class SongsFragment : Fragment() { - private val musicModel: MusicViewModel by activityViewModels { MusicViewModel.Factory(requireActivity().application) } + private val playbackModel: PlaybackViewModel by activityViewModels() + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -31,7 +33,7 @@ class SongsFragment : Fragment() { adapter = SongAdapter( musicModel.songs.value!!, ClickListener { song -> - Log.d(this::class.simpleName, song.name) + playbackModel.updateSong(song) } ) applyDivider() diff --git a/app/src/main/java/org/oxycblt/auxio/theme/ThemeUtils.kt b/app/src/main/java/org/oxycblt/auxio/theme/ThemeUtils.kt index e8d6f3a19..f8981eeeb 100644 --- a/app/src/main/java/org/oxycblt/auxio/theme/ThemeUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/theme/ThemeUtils.kt @@ -87,7 +87,7 @@ fun resolveAttr(context: Context, @AttrRes attr: Int): Int { } // Apply a color to a Menu Item -fun MenuItem.applyColor(@ColorRes color: Int) { +fun MenuItem.applyColor(@ColorInt color: Int) { SpannableString(title).apply { setSpan(ForegroundColorSpan(color), 0, length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE) title = this diff --git a/app/src/main/res/layout/fragment_compact_playback.xml b/app/src/main/res/layout/fragment_compact_playback.xml new file mode 100644 index 000000000..dd4db3c05 --- /dev/null +++ b/app/src/main/res/layout/fragment_compact_playback.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index 64c490f29..20fed8aa4 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -15,6 +15,13 @@ android:layout_height="0dp" android:layout_weight="1" /> + + + app:tabRippleColor="@color/selection_color" + tools:background="@color/control_color" /> \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml index e05dd25a5..b7d84a054 100644 --- a/app/src/main/res/navigation/nav_main.xml +++ b/app/src/main/res/navigation/nav_main.xml @@ -12,10 +12,10 @@ @@ -27,24 +27,24 @@ tools:layout="@layout/fragment_main"> 16dp 8dp + 10dp 16dp 48dp 40dp + 36dp 44dp 56dp 68dp @@ -26,5 +28,7 @@ 18dp + 2dp + 4dp \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 59c7b596f..05b2dd123 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -27,6 +27,10 @@ @dimen/detail_header_size_max + +