Add All Songs fragment

Add an All Songs fragment, also modify the ViewPager structure.
This commit is contained in:
OxygenCobalt 2020-08-26 17:54:45 -06:00
parent c109c3f359
commit c9875a03a9
15 changed files with 321 additions and 46 deletions

View file

@ -1,6 +1,7 @@
package org.oxycblt.auxio package org.oxycblt.auxio
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -10,12 +11,22 @@ import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import org.oxycblt.auxio.databinding.FragmentMainBinding import org.oxycblt.auxio.databinding.FragmentMainBinding
import org.oxycblt.auxio.library.LibraryFragment import org.oxycblt.auxio.library.LibraryFragment
import org.oxycblt.auxio.songs.SongsFragment
// TODO: Placeholder, page count will be dynamic
private const val PAGES = 1
class MainFragment : Fragment() { class MainFragment : Fragment() {
private val shownFragments = listOf(
0, 1
)
private val libraryFragment: LibraryFragment by lazy {
LibraryFragment()
}
private val songsFragment: SongsFragment by lazy {
SongsFragment()
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -28,15 +39,35 @@ class MainFragment : Fragment() {
val adapter = FragmentAdapter(requireActivity()) val adapter = FragmentAdapter(requireActivity())
binding.viewPager.adapter = adapter binding.viewPager.adapter = adapter
Log.d(this::class.simpleName, "Fragment Created.")
return binding.root return binding.root
} }
}
class FragmentAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) { private fun getFragment(pos: Int): Fragment {
override fun getItemCount(): Int = PAGES if (shownFragments.contains(pos)) {
return when (pos) {
0 -> libraryFragment
1 -> songsFragment
override fun createFragment(position: Int): Fragment { else -> libraryFragment
// TODO: Also placeholder, remove when there are other fragments than just library }
return LibraryFragment() }
// Not sure how this would happen but it might
Log.e(
this::class.simpleName,
"Something went terribly wrong while swapping fragments, Substituting with libraryFragment."
)
return libraryFragment
}
inner class FragmentAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
override fun getItemCount(): Int = shownFragments.size
override fun createFragment(position: Int): Fragment {
return getFragment(position)
}
} }
} }

View file

@ -8,7 +8,7 @@ import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.models.Album import org.oxycblt.auxio.music.models.Album
import org.oxycblt.auxio.music.models.Artist import org.oxycblt.auxio.music.models.Artist
class LibraryViewModel() : ViewModel() { class LibraryViewModel : ViewModel() {
private val mArtists = MutableLiveData<List<Artist>>() private val mArtists = MutableLiveData<List<Artist>>()
private var mAlbums = MutableLiveData<List<Album>>() private var mAlbums = MutableLiveData<List<Album>>()

View file

@ -3,6 +3,11 @@ package org.oxycblt.auxio.music
import android.content.ContentUris import android.content.ContentUris
import android.net.Uri import android.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import android.widget.TextView
import androidx.databinding.BindingAdapter
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.models.Album
import org.oxycblt.auxio.music.models.Song
private val ID3_GENRES = arrayOf<String>( private val ID3_GENRES = arrayOf<String>(
"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz", "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz",
@ -41,6 +46,7 @@ fun String.toNamedGenre(): String {
return ID3_GENRES.getOrNull(intGenre) ?: "" return ID3_GENRES.getOrNull(intGenre) ?: ""
} }
// Convert a song to its URI
fun Long.toURI(): Uri { fun Long.toURI(): Uri {
return ContentUris.withAppendedId( return ContentUris.withAppendedId(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
@ -48,9 +54,30 @@ fun Long.toURI(): Uri {
) )
} }
// Convert an albums ID into its album art URI
fun Long.toAlbumArtURI(): Uri { fun Long.toAlbumArtURI(): Uri {
return ContentUris.withAppendedId( return ContentUris.withAppendedId(
Uri.parse("content://media/external/audio/albumart"), Uri.parse("content://media/external/audio/albumart"),
this this
) )
} }
// Format the amount of songs in an album
@BindingAdapter("songCount")
fun TextView.getAlbumSongs(album: Album) {
text = if (album.numSongs < 2) {
context.getString(R.string.label_single_song)
} else {
context.getString(R.string.format_multi_song_count, album.numSongs.toString())
}
}
// Format the artist/album data for a song
@BindingAdapter("songData")
fun TextView.getSongData(song: Song) {
text = context.getString(
R.string.format_song_data,
song.album.artist.name,
song.album.title
)
}

View file

@ -24,8 +24,11 @@ class MusicSorter(
// Find all songs that match the current album title // Find all songs that match the current album title
val albumSongs = songs.filter { it.albumName == album.title } val albumSongs = songs.filter { it.albumName == album.title }
// Then add them to the album, along with refreshing the cover // Then add them to the album
album.songs.addAll(albumSongs) for (song in albumSongs) {
song.album = album
album.songs.add(song)
}
unknownSongs.removeAll(albumSongs) unknownSongs.removeAll(albumSongs)
} }
@ -36,13 +39,13 @@ class MusicSorter(
// Reuse an existing unknown album if one is found // Reuse an existing unknown album if one is found
val unknownAlbum = albums.find { it.title == "" } ?: Album() val unknownAlbum = albums.find { it.title == "" } ?: Album()
unknownAlbum.songs.addAll(unknownSongs)
unknownAlbum.numSongs = unknownAlbum.songs.size
for (song in unknownSongs) { for (song in unknownSongs) {
song.album = unknownAlbum song.album = unknownAlbum
unknownAlbum.songs.add(song)
} }
unknownAlbum.numSongs = unknownAlbum.songs.size
albums.add(unknownAlbum) albums.add(unknownAlbum)
Log.d( Log.d(
@ -61,8 +64,12 @@ class MusicSorter(
// Find all albums that match the current artist name // Find all albums that match the current artist name
val artistAlbums = albums.filter { it.artistName == artist.name } val artistAlbums = albums.filter { it.artistName == artist.name }
// And then add them to the album, along with refreshing the amount of albums // Then add them to the artist, along with refreshing the amount of albums
artist.albums.addAll(artistAlbums) for (album in artistAlbums) {
album.artist = artist
artist.albums.add(album)
}
artist.numAlbums = artist.albums.size artist.numAlbums = artist.albums.size
unknownAlbums.removeAll(artistAlbums) unknownAlbums.removeAll(artistAlbums)
@ -74,14 +81,12 @@ class MusicSorter(
// Reuse an existing unknown artist if one is found // Reuse an existing unknown artist if one is found
val unknownArtist = artists.find { it.name == "" } ?: Artist() val unknownArtist = artists.find { it.name == "" } ?: Artist()
unknownArtist.albums.addAll(unknownAlbums)
unknownArtist.numAlbums = albums.size
for (album in unknownAlbums) { for (album in unknownAlbums) {
album.artist = unknownArtist album.artist = unknownArtist
unknownArtist.albums.add(album)
} }
artists.add(unknownArtist) unknownArtist.numAlbums = albums.size
Log.d( Log.d(
this::class.simpleName, this::class.simpleName,

View file

@ -1,24 +1,12 @@
package org.oxycblt.auxio.recycler package org.oxycblt.auxio.recycler
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.widget.TextView
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.models.Album
@BindingAdapter("songCount")
fun TextView.numSongsToText(album: Album) {
text = if (album.numSongs < 2) {
context.getString(R.string.label_single_song)
} else {
context.getString(R.string.format_multi_song_count, album.numSongs.toString())
}
}
// Apply a custom vertical divider // Apply a custom vertical divider
fun RecyclerView.applyDivider() { fun RecyclerView.applyDivider() {

View file

@ -0,0 +1,26 @@
package org.oxycblt.auxio.recycler
import androidx.recyclerview.widget.RecyclerView
import coil.load
import org.oxycblt.auxio.databinding.SongItemBinding
import org.oxycblt.auxio.music.models.Song
// Generic ViewHolder for a song
class SongViewHolder(
private var binding: SongItemBinding
) : RecyclerView.ViewHolder(binding.root) {
// Bind the view w/new data
fun bind(song: Song) {
binding.song = song
// Load the album cover
binding.cover.load(song.album.coverUri) {
crossfade(true)
placeholder(android.R.color.transparent)
error(android.R.color.transparent)
}
binding.executePendingBindings()
}
}

View file

@ -0,0 +1,40 @@
package org.oxycblt.auxio.songs
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import org.oxycblt.auxio.databinding.SongItemBinding
import org.oxycblt.auxio.music.models.Song
import org.oxycblt.auxio.recycler.SongViewHolder
class SongDataAdapter : ListAdapter<Song, SongViewHolder>(DiffCallback) {
var data = listOf<Song>()
set(newData) {
field = newData
submitList(data)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder {
return SongViewHolder(
SongItemBinding.inflate(LayoutInflater.from(parent.context))
)
}
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
val Song = getItem(position)
holder.bind(Song)
}
companion object DiffCallback : DiffUtil.ItemCallback<Song>() {
override fun areItemsTheSame(oldItem: Song, newItem: Song): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Song, newItem: Song): Boolean {
return oldItem.id == newItem.id
}
}
}

View file

@ -0,0 +1,45 @@
package org.oxycblt.auxio.songs
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentSongsBinding
import org.oxycblt.auxio.recycler.applyDivider
class SongsFragment : Fragment() {
private val songsModel: SongsViewModel by lazy {
ViewModelProvider(this).get(SongsViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = DataBindingUtil.inflate<FragmentSongsBinding>(
inflater, R.layout.fragment_songs, container, false
)
val adapter = SongDataAdapter()
binding.songRecycler.adapter = adapter
binding.songRecycler.applyDivider()
songsModel.songs.observe(
viewLifecycleOwner,
Observer {
adapter.data = it
}
)
Log.d(this::class.simpleName, "Fragment created.")
return binding.root
}
}

View file

@ -0,0 +1,22 @@
package org.oxycblt.auxio.songs
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.models.Song
class SongsViewModel : ViewModel() {
private val mSongs = MutableLiveData<List<Song>>()
val songs: LiveData<List<Song>> get() = mSongs
init {
val repo = MusicRepository.getInstance()
mSongs.value = repo.songs
Log.d(this::class.simpleName, "ViewModel created.")
}
}

View file

@ -53,7 +53,7 @@
app:layout_constraintStart_toEndOf="@+id/cover" app:layout_constraintStart_toEndOf="@+id/cover"
app:layout_constraintTop_toBottomOf="@+id/album_name" app:layout_constraintTop_toBottomOf="@+id/album_name"
app:songCount="@{album}" app:songCount="@{album}"
tools:text="@string/tools_song_count" /> tools:text="10 Songs" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</layout> </layout>

View file

@ -3,9 +3,10 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
@ -19,14 +20,11 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/library_recycler" android:id="@+id/library_recycler"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent" android:layout_weight="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/album_item" /> tools:listitem="@layout/album_item" />
</androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout>
</layout> </layout>

View file

@ -3,6 +3,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android">
<!-- TODO: Add Animation -->
<data> <data>
<variable <variable
@ -33,12 +35,14 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/inter" android:fontFamily="@font/inter"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary"
app:layout_constraintBottom_toTopOf="@+id/reset_button" app:layout_constraintBottom_toTopOf="@+id/reset_button"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loading_bar" app:layout_constraintTop_toBottomOf="@+id/loading_bar"
tools:text="@string/error_music_load_failed" /> tools:text="Some kind of error." />
<Button <Button
android:id="@+id/reset_button" android:id="@+id/reset_button"

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?android:attr/actionBarSize"
android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal"
app:titleTextAppearance="@style/ToolbarStyle"
app:layout_constraintTop_toTopOf="parent"
app:title="@string/title_all_songs" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/song_recycler"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/song_item" />
</LinearLayout>
</layout>

View file

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="song"
type="org.oxycblt.auxio.music.models.Song" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/padding_medium">
<ImageView
android:id="@+id/cover"
android:layout_width="@dimen/cover_size_compact"
android:layout_height="@dimen/cover_size_compact"
android:contentDescription="@{@string/description_cover_art + song.album.title}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/backgrounds/scenic"
tools:srcCompat="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/album_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_medium"
android:text="@{song.title}"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorPrimary"
app:layout_constraintBottom_toTopOf="@+id/song_count"
app:layout_constraintStart_toEndOf="@+id/cover"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Song Name" />
<TextView
android:id="@+id/song_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_medium"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/cover"
app:layout_constraintTop_toBottomOf="@+id/album_name"
app:songData="@{song}"
tools:text="Artist / Album" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -9,10 +9,10 @@
<string name="label_single_song">1 Song</string> <string name="label_single_song">1 Song</string>
<string name="format_multi_song_count">%s Songs</string> <string name="format_multi_song_count">%s Songs</string>
<string name="format_song_data">%s / %s</string>
<string name="title_library_fragment"><b>Library</b></string> <string name="title_library_fragment">Library</string>
<string name="title_all_songs">All Songs</string>
<string name="description_cover_art">Cover art for </string> <string name="description_cover_art">Cover art for </string>
<string name="tools_song_count">10 Songs</string>
</resources> </resources>