Add All Songs fragment
Add an All Songs fragment, also modify the ViewPager structure.
This commit is contained in:
parent
c109c3f359
commit
c9875a03a9
15 changed files with 321 additions and 46 deletions
|
@ -1,6 +1,7 @@
|
|||
package org.oxycblt.auxio
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -10,12 +11,22 @@ import androidx.fragment.app.FragmentActivity
|
|||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
||||
import org.oxycblt.auxio.library.LibraryFragment
|
||||
|
||||
// TODO: Placeholder, page count will be dynamic
|
||||
private const val PAGES = 1
|
||||
import org.oxycblt.auxio.songs.SongsFragment
|
||||
|
||||
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(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
@ -28,15 +39,35 @@ class MainFragment : Fragment() {
|
|||
val adapter = FragmentAdapter(requireActivity())
|
||||
binding.viewPager.adapter = adapter
|
||||
|
||||
Log.d(this::class.simpleName, "Fragment Created.")
|
||||
|
||||
return binding.root
|
||||
}
|
||||
}
|
||||
|
||||
class FragmentAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
|
||||
override fun getItemCount(): Int = PAGES
|
||||
private fun getFragment(pos: Int): Fragment {
|
||||
if (shownFragments.contains(pos)) {
|
||||
return when (pos) {
|
||||
0 -> libraryFragment
|
||||
1 -> songsFragment
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
// TODO: Also placeholder, remove when there are other fragments than just library
|
||||
return LibraryFragment()
|
||||
else -> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import org.oxycblt.auxio.music.MusicRepository
|
|||
import org.oxycblt.auxio.music.models.Album
|
||||
import org.oxycblt.auxio.music.models.Artist
|
||||
|
||||
class LibraryViewModel() : ViewModel() {
|
||||
class LibraryViewModel : ViewModel() {
|
||||
|
||||
private val mArtists = MutableLiveData<List<Artist>>()
|
||||
private var mAlbums = MutableLiveData<List<Album>>()
|
||||
|
|
|
@ -3,6 +3,11 @@ package org.oxycblt.auxio.music
|
|||
import android.content.ContentUris
|
||||
import android.net.Uri
|
||||
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>(
|
||||
"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz",
|
||||
|
@ -41,6 +46,7 @@ fun String.toNamedGenre(): String {
|
|||
return ID3_GENRES.getOrNull(intGenre) ?: ""
|
||||
}
|
||||
|
||||
// Convert a song to its URI
|
||||
fun Long.toURI(): Uri {
|
||||
return ContentUris.withAppendedId(
|
||||
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 {
|
||||
return ContentUris.withAppendedId(
|
||||
Uri.parse("content://media/external/audio/albumart"),
|
||||
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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -24,8 +24,11 @@ class MusicSorter(
|
|||
// Find all songs that match the current album title
|
||||
val albumSongs = songs.filter { it.albumName == album.title }
|
||||
|
||||
// Then add them to the album, along with refreshing the cover
|
||||
album.songs.addAll(albumSongs)
|
||||
// Then add them to the album
|
||||
for (song in albumSongs) {
|
||||
song.album = album
|
||||
album.songs.add(song)
|
||||
}
|
||||
|
||||
unknownSongs.removeAll(albumSongs)
|
||||
}
|
||||
|
@ -36,13 +39,13 @@ class MusicSorter(
|
|||
// Reuse an existing unknown album if one is found
|
||||
val unknownAlbum = albums.find { it.title == "" } ?: Album()
|
||||
|
||||
unknownAlbum.songs.addAll(unknownSongs)
|
||||
unknownAlbum.numSongs = unknownAlbum.songs.size
|
||||
|
||||
for (song in unknownSongs) {
|
||||
song.album = unknownAlbum
|
||||
unknownAlbum.songs.add(song)
|
||||
}
|
||||
|
||||
unknownAlbum.numSongs = unknownAlbum.songs.size
|
||||
|
||||
albums.add(unknownAlbum)
|
||||
|
||||
Log.d(
|
||||
|
@ -61,8 +64,12 @@ class MusicSorter(
|
|||
// Find all albums that match the current artist name
|
||||
val artistAlbums = albums.filter { it.artistName == artist.name }
|
||||
|
||||
// And then add them to the album, along with refreshing the amount of albums
|
||||
artist.albums.addAll(artistAlbums)
|
||||
// Then add them to the artist, along with refreshing the amount of albums
|
||||
for (album in artistAlbums) {
|
||||
album.artist = artist
|
||||
artist.albums.add(album)
|
||||
}
|
||||
|
||||
artist.numAlbums = artist.albums.size
|
||||
|
||||
unknownAlbums.removeAll(artistAlbums)
|
||||
|
@ -74,14 +81,12 @@ class MusicSorter(
|
|||
// Reuse an existing unknown artist if one is found
|
||||
val unknownArtist = artists.find { it.name == "" } ?: Artist()
|
||||
|
||||
unknownArtist.albums.addAll(unknownAlbums)
|
||||
unknownArtist.numAlbums = albums.size
|
||||
|
||||
for (album in unknownAlbums) {
|
||||
album.artist = unknownArtist
|
||||
unknownArtist.albums.add(album)
|
||||
}
|
||||
|
||||
artists.add(unknownArtist)
|
||||
unknownArtist.numAlbums = albums.size
|
||||
|
||||
Log.d(
|
||||
this::class.simpleName,
|
||||
|
|
|
@ -1,24 +1,12 @@
|
|||
package org.oxycblt.auxio.recycler
|
||||
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.databinding.BindingAdapter
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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
|
||||
fun RecyclerView.applyDivider() {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
40
app/src/main/java/org/oxycblt/auxio/songs/SongDataAdapter.kt
Normal file
40
app/src/main/java/org/oxycblt/auxio/songs/SongDataAdapter.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
45
app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt
Normal file
45
app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt
Normal 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
|
||||
}
|
||||
}
|
22
app/src/main/java/org/oxycblt/auxio/songs/SongsViewModel.kt
Normal file
22
app/src/main/java/org/oxycblt/auxio/songs/SongsViewModel.kt
Normal 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.")
|
||||
}
|
||||
}
|
|
@ -53,7 +53,7 @@
|
|||
app:layout_constraintStart_toEndOf="@+id/cover"
|
||||
app:layout_constraintTop_toBottomOf="@+id/album_name"
|
||||
app:songCount="@{album}"
|
||||
tools:text="@string/tools_song_count" />
|
||||
tools:text="10 Songs" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
|
@ -3,9 +3,10 @@
|
|||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
|
@ -19,14 +20,11 @@
|
|||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/library_recycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar"
|
||||
android:layout_weight="1"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/album_item" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
</layout>
|
|
@ -3,6 +3,8 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- TODO: Add Animation -->
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
|
@ -33,12 +35,14 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/inter"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
app:layout_constraintBottom_toTopOf="@+id/reset_button"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loading_bar"
|
||||
tools:text="@string/error_music_load_failed" />
|
||||
tools:text="Some kind of error." />
|
||||
|
||||
<Button
|
||||
android:id="@+id/reset_button"
|
||||
|
|
30
app/src/main/res/layout/fragment_songs.xml
Normal file
30
app/src/main/res/layout/fragment_songs.xml
Normal 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>
|
59
app/src/main/res/layout/song_item.xml
Normal file
59
app/src/main/res/layout/song_item.xml
Normal 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>
|
|
@ -9,10 +9,10 @@
|
|||
<string name="label_single_song">1 Song</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="tools_song_count">10 Songs</string>
|
||||
</resources>
|
Loading…
Reference in a new issue