list: add editable list listener

Add a listener for the editable lists in the queue and tab config
views.

This simply reduces the amount of duplicated code within both of those
views.
This commit is contained in:
Alexander Capehart 2022-12-31 13:47:13 -07:00
parent f4aa20b2f1
commit dc46c49f07
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
29 changed files with 147 additions and 128 deletions

View file

@ -29,9 +29,9 @@ jobs:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
- name: Grant execute permission for gradlew - name: Grant execute permission for gradlew
run: chmod +x gradlew run: chmod +x gradlew
- name: Build Debug APK with Gradle - name: Build debug APK with Gradle
run: ./gradlew app:packageDebug run: ./gradlew app:packageDebug
- name: Upload a Build Artifact - name: Upload debug APK artifact
uses: actions/upload-artifact@v3.1.1 uses: actions/upload-artifact@v3.1.1
with: with:
name: Auxio_Canary name: Auxio_Canary

View file

@ -10,6 +10,9 @@
- Value lists are now properly localized - Value lists are now properly localized
- Queue no longer primarily shows previous songs when opened - Queue no longer primarily shows previous songs when opened
#### What's Fixed
- Fixed mangled multi-value ID3v2 tags when UTF-16 is used
## 3.0.0 ## 3.0.0
#### What's New #### What's New

View file

@ -227,7 +227,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
* @param listener A [SelectableListListener] to bind interactions to. * @param listener A [SelectableListListener] to bind interactions to.
*/ */
fun bind(song: Song, listener: SelectableListListener) { fun bind(song: Song, listener: SelectableListListener) {
listener.bind(this, song, binding.songMenu) listener.bind(song, this, menuButton = binding.songMenu)
binding.songTrack.apply { binding.songTrack.apply {
if (song.track != null) { if (song.track != null) {

View file

@ -184,7 +184,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
* @param listener An [SelectableListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
fun bind(album: Album, listener: SelectableListListener) { fun bind(album: Album, listener: SelectableListListener) {
listener.bind(this, album, binding.parentMenu) listener.bind(album, this, menuButton = binding.parentMenu)
binding.parentImage.bind(album) binding.parentImage.bind(album)
binding.parentName.text = album.resolveName(binding.context) binding.parentName.text = album.resolveName(binding.context)
binding.parentInfo.text = binding.parentInfo.text =
@ -236,7 +236,7 @@ private class ArtistSongViewHolder private constructor(private val binding: Item
* @param listener An [SelectableListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
fun bind(song: Song, listener: SelectableListListener) { fun bind(song: Song, listener: SelectableListListener) {
listener.bind(this, song, binding.songMenu) listener.bind(song, this, menuButton = binding.songMenu)
binding.songAlbumCover.bind(song) binding.songAlbumCover.bind(song)
binding.songName.text = song.resolveName(binding.context) binding.songName.text = song.resolveName(binding.context)
binding.songInfo.text = song.album.resolveName(binding.context) binding.songInfo.text = song.album.resolveName(binding.context)

View file

@ -354,6 +354,7 @@ class HomeFragment :
} }
} }
is Indexer.Response.NoMusic -> { is Indexer.Response.NoMusic -> {
// TODO: Move this state to the list fragments (makes life easier)
logD("Updating UI to Response.NoMusic state") logD("Updating UI to Response.NoMusic state")
binding.homeIndexingStatus.text = context.getString(R.string.err_no_music) binding.homeIndexingStatus.text = context.getString(R.string.err_no_music)

View file

@ -17,6 +17,7 @@
package org.oxycblt.auxio.home.tabs package org.oxycblt.auxio.home.tabs
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
@ -25,7 +26,7 @@ import org.oxycblt.auxio.util.logE
* @param mode The type of list in the home view this instance corresponds to. * @param mode The type of list in the home view this instance corresponds to.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
sealed class Tab(open val mode: MusicMode) { sealed class Tab(open val mode: MusicMode) : Item {
/** /**
* A visible tab. This will be visible in the home and tab configuration views. * A visible tab. This will be visible in the home and tab configuration views.
* @param mode The type of list in the home view this instance corresponds to. * @param mode The type of list in the home view this instance corresponds to.

View file

@ -18,21 +18,22 @@
package org.oxycblt.auxio.home.tabs package org.oxycblt.auxio.home.tabs
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemTabBinding import org.oxycblt.auxio.databinding.ItemTabBinding
import org.oxycblt.auxio.list.EditableListListener
import org.oxycblt.auxio.list.recycler.DialogRecyclerView import org.oxycblt.auxio.list.recycler.DialogRecyclerView
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
/** /**
* A [RecyclerView.Adapter] that displays an array of [Tab]s open for configuration. * A [RecyclerView.Adapter] that displays an array of [Tab]s open for configuration.
* @param listener A [Listener] for tab interactions. * @param listener A [EditableListListener] for tab interactions.
*/ */
class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewHolder>() { class TabAdapter(private val listener: EditableListListener) :
RecyclerView.Adapter<TabViewHolder>() {
/** The current array of [Tab]s. */ /** The current array of [Tab]s. */
var tabs = arrayOf<Tab>() var tabs = arrayOf<Tab>()
private set private set
@ -75,23 +76,6 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewH
notifyItemMoved(a, b) notifyItemMoved(a, b)
} }
/** A listener for interactions specific to tab configuration. */
interface Listener {
/**
* Called when a tab is clicked, requesting that the visibility should be inverted (i.e
* Visible -> Invisible and vice versa).
* @param tabMode The [MusicMode] of the tab clicked.
*/
fun onToggleVisibility(tabMode: MusicMode)
/**
* Called when the drag handle on a [RecyclerView.ViewHolder] is clicked, requesting that a
* drag should be started.
* @param viewHolder The [RecyclerView.ViewHolder] to start dragging.
*/
fun onPickUp(viewHolder: RecyclerView.ViewHolder)
}
private companion object { private companion object {
val PAYLOAD_TAB_CHANGED = Any() val PAYLOAD_TAB_CHANGED = Any()
} }
@ -106,12 +90,11 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) :
/** /**
* Bind new data to this instance. * Bind new data to this instance.
* @param tab The new [Tab] to bind. * @param tab The new [Tab] to bind.
* @param listener A [TabAdapter.Listener] to bind interactions to. * @param listener A [EditableListListener] to bind interactions to.
*/ */
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
fun bind(tab: Tab, listener: TabAdapter.Listener) { fun bind(tab: Tab, listener: EditableListListener) {
binding.root.setOnClickListener { listener.onToggleVisibility(tab.mode) } listener.bind(tab, this, dragHandle = binding.tabDragHandle)
binding.tabCheckBox.apply { binding.tabCheckBox.apply {
// Update the CheckBox name to align with the mode // Update the CheckBox name to align with the mode
setText( setText(
@ -126,15 +109,6 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) :
// the tab data since they are in the same data structure (Tab) // the tab data since they are in the same data structure (Tab)
isChecked = tab is Tab.Visible isChecked = tab is Tab.Visible
} }
// Set up the drag handle to start a drag whenever it is touched.
binding.tabDragHandle.setOnTouchListener { _, motionEvent ->
binding.tabDragHandle.performClick()
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
listener.onPickUp(this)
true
} else false
}
} }
companion object { companion object {

View file

@ -25,7 +25,8 @@ import androidx.recyclerview.widget.RecyclerView
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.music.MusicMode import org.oxycblt.auxio.list.EditableListListener
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -34,7 +35,7 @@ import org.oxycblt.auxio.util.logD
* A [ViewBindingDialogFragment] that allows the user to modify the home [Tab] configuration. * A [ViewBindingDialogFragment] that allows the user to modify the home [Tab] configuration.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAdapter.Listener { class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), EditableListListener {
private val tabAdapter = TabAdapter(this) private val tabAdapter = TabAdapter(this)
private var touchHelper: ItemTouchHelper? = null private var touchHelper: ItemTouchHelper? = null
@ -80,12 +81,11 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
binding.tabRecycler.adapter = null binding.tabRecycler.adapter = null
} }
override fun onToggleVisibility(tabMode: MusicMode) { override fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) {
logD("Toggling tab $tabMode") check(item is Tab) { "Unexpected datatype: ${item::class.java}" }
// We will need the exact index of the tab to update on in order to // We will need the exact index of the tab to update on in order to
// notify the adapter of the change. // notify the adapter of the change.
val index = tabAdapter.tabs.indexOfFirst { it.mode == tabMode } val index = tabAdapter.tabs.indexOfFirst { it.mode == item.mode }
val tab = tabAdapter.tabs[index] val tab = tabAdapter.tabs[index]
tabAdapter.setTab( tabAdapter.setTab(
index, index,

View file

@ -107,7 +107,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
// Playback indicator should sit above the inner StyledImageView and custom view/ // Playback indicator should sit above the inner StyledImageView and custom view/
addView(playbackIndicatorView) addView(playbackIndicatorView)
// Selction indicator should never be obscured, so place it at the top. // Selection indicator should never be obscured, so place it at the top.
addView( addView(
selectionIndicatorView, selectionIndicatorView,
LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply { LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {

View file

@ -22,6 +22,7 @@ import android.view.View
import androidx.annotation.MenuRes import androidx.annotation.MenuRes
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import org.oxycblt.auxio.MainFragmentDirections import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
@ -53,7 +54,7 @@ abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), Selecta
*/ */
abstract fun onRealClick(music: Music) abstract fun onRealClick(music: Music)
override fun onClick(item: Item) { override fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) {
check(item is Music) { "Unexpected datatype: ${item::class.simpleName}" } check(item is Music) { "Unexpected datatype: ${item::class.simpleName}" }
if (selectionModel.selected.value.isNotEmpty()) { if (selectionModel.selected.value.isNotEmpty()) {
// Map clicking an item to selecting an item when items are already selected. // Map clicking an item to selecting an item when items are already selected.

View file

@ -17,8 +17,8 @@
package org.oxycblt.auxio.list package org.oxycblt.auxio.list
import android.view.MotionEvent
import android.view.View import android.view.View
import android.widget.Button
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
/** /**
@ -26,13 +26,63 @@ import androidx.recyclerview.widget.RecyclerView
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
interface ClickableListListener { interface ClickableListListener {
// TODO: Supply a ViewHolder on clicks
// (allows editable lists to be standardized into a listener.)
/** /**
* Called when an [Item] in the list is clicked. * Called when an [Item] in the list is clicked.
* @param item The [Item] that was clicked. * @param item The [Item] that was clicked.
* @param viewHolder The [RecyclerView.ViewHolder] of the item that was clicked.
*/ */
fun onClick(item: Item) fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder)
/**
* Binds this instance to a list item.
* @param item The [Item] that this list entry is bound to.
* @param viewHolder The [RecyclerView.ViewHolder] of the item that was clicked.
* @param bodyView The [View] containing the main body of the list item. Any click events on
* this [View] are routed to the listener. Defaults to the root view.
*/
fun bind(
item: Item,
viewHolder: RecyclerView.ViewHolder,
bodyView: View = viewHolder.itemView
) {
bodyView.setOnClickListener { onClick(item, viewHolder) }
}
}
/**
* An extension of [ClickableListListener] that enables list editing functionality.
* @author Alexander Capehart (OxygenCobalt)
*/
interface EditableListListener : ClickableListListener {
/**
* Called when a [RecyclerView.ViewHolder] requests that it should be dragged.
* @param viewHolder The [RecyclerView.ViewHolder] that should start being dragged.
*/
fun onPickUp(viewHolder: RecyclerView.ViewHolder)
/**
* Binds this instance to a list item.
* @param item The [Item] that this list entry is bound to.
* @param viewHolder The [RecyclerView.ViewHolder] to bind.
* @param bodyView The [View] containing the main body of the list item. Any click events on
* this [View] are routed to the listener. Defaults to the root view.
* @param dragHandle A touchable [View]. Any drag on this view will start a drag event.
*/
fun bind(
item: Item,
viewHolder: RecyclerView.ViewHolder,
bodyView: View = viewHolder.itemView,
dragHandle: View
) {
bind(item, viewHolder, bodyView)
dragHandle.setOnTouchListener { _, motionEvent ->
dragHandle.performClick()
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
onPickUp(viewHolder)
true
} else false
}
}
} }
/** /**
@ -55,19 +105,23 @@ interface SelectableListListener : ClickableListListener {
/** /**
* Binds this instance to a list item. * Binds this instance to a list item.
* @param viewHolder The [RecyclerView.ViewHolder] to bind.
* @param item The [Item] that this list entry is bound to. * @param item The [Item] that this list entry is bound to.
* @param menuButton A [Button] that opens a menu. * @param viewHolder The [RecyclerView.ViewHolder] to bind.
* @param bodyView The [View] containing the main body of the list item. Any click events on
* this [View] are routed to the listener. Defaults to the root view.
* @param menuButton A clickable [View]. Any click events on this [View] will open a menu.
*/ */
fun bind(viewHolder: RecyclerView.ViewHolder, item: Item, menuButton: Button) { fun bind(
viewHolder.itemView.apply { item: Item,
// Map clicks to the click listener. viewHolder: RecyclerView.ViewHolder,
setOnClickListener { onClick(item) } bodyView: View = viewHolder.itemView,
// Map long clicks to the selection listener. menuButton: View
setOnLongClickListener { ) {
onSelect(item) bind(item, viewHolder, bodyView)
true // Map long clicks to the selection listener.
} bodyView.setOnLongClickListener {
onSelect(item)
true
} }
// Map the menu button to the menu opening listener. // Map the menu button to the menu opening listener.
menuButton.setOnClickListener { onOpenMenu(item, it) } menuButton.setOnClickListener { onOpenMenu(item, it) }

View file

@ -46,7 +46,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
* @param listener An [SelectableListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
fun bind(song: Song, listener: SelectableListListener) { fun bind(song: Song, listener: SelectableListListener) {
listener.bind(this, song, binding.songMenu) listener.bind(song, this, menuButton = binding.songMenu)
binding.songAlbumCover.bind(song) binding.songAlbumCover.bind(song)
binding.songName.text = song.resolveName(binding.context) binding.songName.text = song.resolveName(binding.context)
binding.songInfo.text = song.resolveArtistContents(binding.context) binding.songInfo.text = song.resolveArtistContents(binding.context)
@ -93,7 +93,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
* @param listener An [SelectableListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
fun bind(album: Album, listener: SelectableListListener) { fun bind(album: Album, listener: SelectableListListener) {
listener.bind(this, album, binding.parentMenu) listener.bind(album, this, menuButton = binding.parentMenu)
binding.parentImage.bind(album) binding.parentImage.bind(album)
binding.parentName.text = album.resolveName(binding.context) binding.parentName.text = album.resolveName(binding.context)
binding.parentInfo.text = album.resolveArtistContents(binding.context) binding.parentInfo.text = album.resolveArtistContents(binding.context)
@ -142,7 +142,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
* @param listener An [SelectableListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
fun bind(artist: Artist, listener: SelectableListListener) { fun bind(artist: Artist, listener: SelectableListListener) {
listener.bind(this, artist, binding.parentMenu) listener.bind(artist, this, menuButton = binding.parentMenu)
binding.parentImage.bind(artist) binding.parentImage.bind(artist)
binding.parentName.text = artist.resolveName(binding.context) binding.parentName.text = artist.resolveName(binding.context)
binding.parentInfo.text = binding.parentInfo.text =
@ -201,7 +201,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
* @param listener An [SelectableListListener] to bind interactions to. * @param listener An [SelectableListListener] to bind interactions to.
*/ */
fun bind(genre: Genre, listener: SelectableListListener) { fun bind(genre: Genre, listener: SelectableListListener) {
listener.bind(this, genre, binding.parentMenu) listener.bind(genre, this, menuButton = binding.parentMenu)
binding.parentImage.bind(genre) binding.parentImage.bind(genre)
binding.parentName.text = genre.resolveName(binding.context) binding.parentName.text = genre.resolveName(binding.context)
binding.parentInfo.text = binding.parentInfo.text =

View file

@ -282,7 +282,7 @@ sealed class Music : Item {
private companion object { private companion object {
/** Cached collator instance re-used with [makeCollationKeyImpl]. */ /** Cached collator instance re-used with [makeCollationKeyImpl]. */
val COLLATOR = Collator.getInstance().apply { strength = Collator.PRIMARY } val COLLATOR: Collator = Collator.getInstance().apply { strength = Collator.PRIMARY }
} }
} }

View file

@ -68,7 +68,7 @@ class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) :
* @param listener A [ClickableListListener] to bind interactions to. * @param listener A [ClickableListListener] to bind interactions to.
*/ */
fun bind(artist: Artist, listener: ClickableListListener) { fun bind(artist: Artist, listener: ClickableListListener) {
binding.root.setOnClickListener { listener.onClick(artist) } listener.bind(artist, this)
binding.pickerImage.bind(artist) binding.pickerImage.bind(artist)
binding.pickerName.text = artist.resolveName(binding.context) binding.pickerName.text = artist.resolveName(binding.context)
} }

View file

@ -20,6 +20,7 @@ package org.oxycblt.auxio.music.picker
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
@ -40,8 +41,8 @@ class ArtistNavigationPickerDialog : ArtistPickerDialog() {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
} }
override fun onClick(item: Item) { override fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) {
super.onClick(item) super.onClick(item, viewHolder)
check(item is Artist) { "Unexpected datatype: ${item::class.simpleName}" } check(item is Artist) { "Unexpected datatype: ${item::class.simpleName}" }
// User made a choice, navigate to it. // User made a choice, navigate to it.
navModel.exploreNavigateTo(item) navModel.exploreNavigateTo(item)

View file

@ -22,6 +22,7 @@ import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.list.ClickableListListener
@ -67,7 +68,7 @@ abstract class ArtistPickerDialog :
binding.pickerRecycler.adapter = null binding.pickerRecycler.adapter = null
} }
override fun onClick(item: Item) { override fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) {
findNavController().navigateUp() findNavController().navigateUp()
} }
} }

View file

@ -19,6 +19,7 @@ package org.oxycblt.auxio.music.picker
import android.os.Bundle import android.os.Bundle
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
@ -41,8 +42,8 @@ class ArtistPlaybackPickerDialog : ArtistPickerDialog() {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
} }
override fun onClick(item: Item) { override fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) {
super.onClick(item) super.onClick(item, viewHolder)
// User made a choice, play the given song from that artist. // User made a choice, play the given song from that artist.
check(item is Artist) { "Unexpected datatype: ${item::class.simpleName}" } check(item is Artist) { "Unexpected datatype: ${item::class.simpleName}" }
val song = pickerModel.currentItem.value val song = pickerModel.currentItem.value

View file

@ -68,7 +68,7 @@ class GenreChoiceViewHolder(private val binding: ItemPickerChoiceBinding) :
* @param listener A [ClickableListListener] to bind interactions to. * @param listener A [ClickableListListener] to bind interactions to.
*/ */
fun bind(genre: Genre, listener: ClickableListListener) { fun bind(genre: Genre, listener: ClickableListListener) {
binding.root.setOnClickListener { listener.onClick(genre) } listener.bind(genre, this)
binding.pickerImage.bind(genre) binding.pickerImage.bind(genre)
binding.pickerName.text = genre.resolveName(binding.context) binding.pickerName.text = genre.resolveName(binding.context)
} }

View file

@ -23,6 +23,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.list.ClickableListListener
@ -74,7 +75,7 @@ class GenrePlaybackPickerDialog :
binding.pickerRecycler.adapter = null binding.pickerRecycler.adapter = null
} }
override fun onClick(item: Item) { override fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) {
// User made a choice, play the given song from that genre. // User made a choice, play the given song from that genre.
check(item is Genre) { "Unexpected datatype: ${item::class.simpleName}" } check(item is Genre) { "Unexpected datatype: ${item::class.simpleName}" }
val song = pickerModel.currentItem.value val song = pickerModel.currentItem.value

View file

@ -72,7 +72,6 @@ class IndexingNotification(private val context: Context) :
// Determinate state, show an active progress meter. Since these updates arrive // Determinate state, show an active progress meter. Since these updates arrive
// highly rapidly, only update every 1.5 seconds to prevent notification rate // highly rapidly, only update every 1.5 seconds to prevent notification rate
// limiting. // limiting.
// TODO: Can I port this to the playback notification somehow?
val now = SystemClock.elapsedRealtime() val now = SystemClock.elapsedRealtime()
if (lastUpdateTime > -1 && (now - lastUpdateTime) < 1500) { if (lastUpdateTime > -1 && (now - lastUpdateTime) < 1500) {
return false return false

View file

@ -198,7 +198,7 @@ class IndexerService :
// 2. If a non-foreground service is killed, the app will probably still be alive, // 2. If a non-foreground service is killed, the app will probably still be alive,
// and thus the music library will not be updated at all. // and thus the music library will not be updated at all.
// TODO: Assuming I unify this with PlaybackService, it's possible that I won't need // TODO: Assuming I unify this with PlaybackService, it's possible that I won't need
// this anymore. // this anymore, or at least I only have to use it when the app task is not removed.
if (!foregroundManager.tryStartForeground(observingNotification)) { if (!foregroundManager.tryStartForeground(observingNotification)) {
observingNotification.post() observingNotification.post()
} }

View file

@ -102,6 +102,7 @@ class PlaybackPanelFragment :
binding.playbackSeekBar.listener = this binding.playbackSeekBar.listener = this
// Set up actions // Set up actions
// TODO: Add better playback button accessibility
binding.playbackRepeat.setOnClickListener { playbackModel.toggleRepeatMode() } binding.playbackRepeat.setOnClickListener { playbackModel.toggleRepeatMode() }
binding.playbackSkipPrev.setOnClickListener { playbackModel.prev() } binding.playbackSkipPrev.setOnClickListener { playbackModel.prev() }
binding.playbackPlayPause.setOnClickListener { playbackModel.toggleIsPlaying() } binding.playbackPlayPause.setOnClickListener { playbackModel.toggleIsPlaying() }

View file

@ -19,7 +19,6 @@ package org.oxycblt.auxio.playback.queue
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.drawable.LayerDrawable import android.graphics.drawable.LayerDrawable
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
@ -27,6 +26,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemQueueSongBinding import org.oxycblt.auxio.databinding.ItemQueueSongBinding
import org.oxycblt.auxio.list.EditableListListener
import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SongViewHolder import org.oxycblt.auxio.list.recycler.SongViewHolder
import org.oxycblt.auxio.list.recycler.SyncListDiffer import org.oxycblt.auxio.list.recycler.SyncListDiffer
@ -38,10 +38,11 @@ import org.oxycblt.auxio.util.inflater
/** /**
* A [RecyclerView.Adapter] that shows an editable list of queue items. * A [RecyclerView.Adapter] that shows an editable list of queue items.
* @param listener A [Listener] to bind interactions to. * @param listener A [EditableListListener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class QueueAdapter(private val listener: Listener) : RecyclerView.Adapter<QueueSongViewHolder>() { class QueueAdapter(private val listener: EditableListListener) :
RecyclerView.Adapter<QueueSongViewHolder>() {
private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFF_CALLBACK) private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFF_CALLBACK)
// Since PlayingIndicator adapter relies on an item value, we cannot use it for this // Since PlayingIndicator adapter relies on an item value, we cannot use it for this
// adapter, as one item can appear at several points in the UI. Use a similar implementation // adapter, as one item can appear at several points in the UI. Use a similar implementation
@ -121,22 +122,6 @@ class QueueAdapter(private val listener: Listener) : RecyclerView.Adapter<QueueS
} }
} }
/** A listener for queue list events. */
interface Listener {
/**
* Called when a [RecyclerView.ViewHolder] in the list as clicked.
* @param viewHolder The [RecyclerView.ViewHolder] that was clicked.
*/
fun onClick(viewHolder: RecyclerView.ViewHolder)
/**
* Called when the drag handle on a [RecyclerView.ViewHolder] is clicked, requesting that a
* drag should be started.
* @param viewHolder The [RecyclerView.ViewHolder] to start dragging.
*/
fun onPickUp(viewHolder: RecyclerView.ViewHolder)
}
private companion object { private companion object {
val PAYLOAD_UPDATE_POSITION = Any() val PAYLOAD_UPDATE_POSITION = Any()
} }
@ -190,27 +175,17 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong
/** /**
* Bind new data to this instance. * Bind new data to this instance.
* @param song The new [Song] to bind. * @param song The new [Song] to bind.
* @param listener A [QueueAdapter.Listener] to bind interactions to. * @param listener A [EditableListListener] to bind interactions to.
*/ */
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
fun bind(song: Song, listener: QueueAdapter.Listener) { fun bind(song: Song, listener: EditableListListener) {
binding.body.setOnClickListener { listener.onClick(this) } listener.bind(song, this, bodyView, binding.songDragHandle)
binding.songAlbumCover.bind(song) binding.songAlbumCover.bind(song)
binding.songName.text = song.resolveName(binding.context) binding.songName.text = song.resolveName(binding.context)
binding.songInfo.text = song.resolveArtistContents(binding.context) binding.songInfo.text = song.resolveArtistContents(binding.context)
// Not swiping this ViewHolder if it's being re-bound, ensure that the background is // Not swiping this ViewHolder if it's being re-bound, ensure that the background is
// not visible. See QueueDragCallback for why this is done. // not visible. See QueueDragCallback for why this is done.
binding.background.isInvisible = true binding.background.isInvisible = true
// Set up the drag handle to start a drag whenever it is touched.
binding.songDragHandle.setOnTouchListener { _, motionEvent ->
binding.songDragHandle.performClick()
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
listener.onPickUp(this)
true
} else false
}
} }
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) { override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {

View file

@ -26,6 +26,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlin.math.min import kotlin.math.min
import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.databinding.FragmentQueueBinding
import org.oxycblt.auxio.list.EditableListListener
import org.oxycblt.auxio.list.Item
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.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
@ -37,7 +39,7 @@ import org.oxycblt.auxio.util.logD
* A [ViewBindingFragment] that displays an editable queue. * A [ViewBindingFragment] that displays an editable queue.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueAdapter.Listener { class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), EditableListListener {
private val queueModel: QueueViewModel by activityViewModels() private val queueModel: QueueViewModel by activityViewModels()
private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val queueAdapter = QueueAdapter(this) private val queueAdapter = QueueAdapter(this)
@ -79,8 +81,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueAdapter.
binding.queueRecycler.adapter = null binding.queueRecycler.adapter = null
} }
override fun onClick(viewHolder: RecyclerView.ViewHolder) { override fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) {
// Clicking on a queue item should start playing it.
queueModel.goto(viewHolder.bindingAdapterPosition) queueModel.goto(viewHolder.bindingAdapterPosition)
} }

View file

@ -94,13 +94,13 @@ class AccentViewHolder private constructor(private val binding: ItemAccentBindin
* @param listener A [ClickableListListener] to bind interactions to. * @param listener A [ClickableListListener] to bind interactions to.
*/ */
fun bind(accent: Accent, listener: ClickableListListener) { fun bind(accent: Accent, listener: ClickableListListener) {
listener.bind(accent, this, binding.accent)
binding.accent.apply { binding.accent.apply {
setOnClickListener { listener.onClick(accent) }
backgroundTintList = context.getColorCompat(accent.primary)
// Add a Tooltip based on the content description so that the purpose of this // Add a Tooltip based on the content description so that the purpose of this
// button can be clear. // button can be clear.
contentDescription = context.getString(accent.name) contentDescription = context.getString(accent.name)
TooltipCompat.setTooltipText(this, contentDescription) TooltipCompat.setTooltipText(this, contentDescription)
backgroundTintList = context.getColorCompat(accent.primary)
} }
} }

View file

@ -20,6 +20,7 @@ package org.oxycblt.auxio.ui.accent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
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
@ -79,7 +80,7 @@ class AccentCustomizeDialog :
binding.accentRecycler.adapter = null binding.accentRecycler.adapter = null
} }
override fun onClick(item: Item) { override fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) {
check(item is Accent) { "Unexpected datatype: ${item::class.java}" } check(item is Accent) { "Unexpected datatype: ${item::class.java}" }
accentAdapter.setSelectedAccent(item) accentAdapter.setSelectedAccent(item)
} }

View file

@ -60,9 +60,9 @@
<string name="lbl_mixtapes">Mixtapes</string> <string name="lbl_mixtapes">Mixtapes</string>
<!-- As in the collection of music --> <!-- As in the collection of music -->
<string name="lbl_mixtape">Mixtape</string> <string name="lbl_mixtape">Mixtape</string>
<!-- As in a compilation of several performances that blend into a single continuous flow of music --> <!-- As in a compilation of several performances that blend into a single continuous flow of music (Also known as DJ Mixes) -->
<string name="lbl_mixes">Mixes</string> <string name="lbl_mixes">Mixes</string>
<!-- As in a compilation of several performances that blend into a single continuous flow of music --> <!-- As in a compilation of several performances that blend into a single continuous flow of music (Also known as DJ Mixes) -->
<string name="lbl_mix">Mix</string> <string name="lbl_mix">Mix</string>
<!-- As in music that was performed live --> <!-- As in music that was performed live -->
@ -341,7 +341,10 @@
<!-- Format Namespace | Value formatting/plurals --> <!-- Format Namespace | Value formatting/plurals -->
<eat-comment /> <eat-comment />
<!-- Comma (,) separator should be localized (For example, "、" in japanese) --> <!--
Comma (,) separator should be localized (For example, "、" in japanese).
Do not use "and" or equivalents.
-->
<string name="fmt_list">%1$s, %2$s</string> <string name="fmt_list">%1$s, %2$s</string>
<!-- As in an amount of items that are selected --> <!-- As in an amount of items that are selected -->
@ -365,7 +368,7 @@
<string name="fmt_lib_album_count">Albums loaded: %d</string> <string name="fmt_lib_album_count">Albums loaded: %d</string>
<string name="fmt_lib_artist_count">Artists loaded: %d</string> <string name="fmt_lib_artist_count">Artists loaded: %d</string>
<string name="fmt_lib_genre_count">Genres loaded: %d</string> <string name="fmt_lib_genre_count">Genres loaded: %d</string>
<!-- AS in the total duration of all songs in the music library --> <!-- As in the total duration of all songs in the music library -->
<string name="fmt_lib_total_duration">Total duration: %s</string> <string name="fmt_lib_total_duration">Total duration: %s</string>
<plurals name="fmt_song_count"> <plurals name="fmt_song_count">

View file

@ -1,4 +1,4 @@
Auxio is a local music player with a fast, reliable UI/UX without the many useless features present in other music players. Built off of <a href="https://exoplayer.dev/">Exoplayer</a>, Auxio has superior library support and listening quality compared to other apps that use outdated android functionality. In short, It plays music. Auxio is a local music player with a fast, reliable UI/UX without the many useless features present in other music players. Built off of <a href="https://exoplayer.dev/">Exoplayer</a>, Auxio has superior library support and listening quality compared to other apps that use outdated android functionality. In short, <b>It plays music</b>.
<b>Features</b> <b>Features</b>

View file

@ -19,13 +19,14 @@ import re
# WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE FLAC EXTENSION AND # WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE FLAC EXTENSION AND
# THE GRADLE DEPENDENCY. IF NOT, VERY UNFRIENDLY BUILD FAILURES AND CRASHES MAY ENSUE. # THE GRADLE DEPENDENCY. IF NOT, VERY UNFRIENDLY BUILD FAILURES AND CRASHES MAY ENSUE.
# EXO_VERSION = "2.18.1" # EXO_VERSION = "2.18.2"
FLAC_VERSION = "1.3.2" FLAC_VERSION = "1.3.2"
FATAL="\033[1;31m" OK="\033[1;32m" # Bold green
WARN="\033[1;91m" FATAL="\033[1;31m" # Bold red
INFO="\033[1;94m" WARN="\033[1;33m" # Bold yellow
OK="\033[1;92m" RUN="\033[1;34m" # Bold blue
INFO="\033[1m" # Bold white
NC="\033[0m" NC="\033[0m"
# We do some shell scripting later on, so we can't support windows. # We do some shell scripting later on, so we can't support windows.
@ -36,7 +37,7 @@ if system not in ["Linux", "Darwin"]:
sys.exit(1) sys.exit(1)
def sh(cmd): def sh(cmd):
print(INFO + "execute: " + NC + cmd) print(RUN + "execute: " + NC + cmd)
code = subprocess.call(["sh", "-c", "set -e; " + cmd]) code = subprocess.call(["sh", "-c", "set -e; " + cmd])
if code != 0: if code != 0:
print(FATAL + "fatal:" + NC + " command failed with exit code " + str(code)) print(FATAL + "fatal:" + NC + " command failed with exit code " + str(code))
@ -79,11 +80,11 @@ if ndk_path is None or not os.path.isfile(os.path.join(ndk_path, "ndk-build")):
"candidates were found however:") "candidates were found however:")
for i, candidate in enumerate(candidates): for i, candidate in enumerate(candidates):
print("[" + str(i) + "] " + candidate) print("[" + str(i) + "] " + candidate)
print(WARN + "info:" + NC + " NDK r21e is recommended for this script. Other " + print(INFO + "info:" + NC + " NDK r21e is recommended for this script. Other " +
"NDKs may result in unexpected behavior.") "NDKs may result in unexpected behavior.")
try: try:
ndk_path = candidates[int(input("enter the ndk to use [default 0]: "))] ndk_path = candidates[int(input("enter the ndk to use [default 0]: "))]
except: except ValueError:
ndk_path = candidates[0] ndk_path = candidates[0]
else: else:
print(FATAL + "fatal:" + NC + " the android ndk was not installed at a " + print(FATAL + "fatal:" + NC + " the android ndk was not installed at a " +