ui: re-add more button

Re-enable the more button in preparation for multi-select.
This commit is contained in:
Alexander Capehart 2022-11-22 10:58:41 -07:00
parent 2242c413d8
commit a3772b65c0
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
23 changed files with 87 additions and 122 deletions

View file

@ -30,13 +30,12 @@
- Fixed issue where the scroll popup would not display correctly in landscape mode [#230]
- Fixed issue where the playback progress would continue in the notification when
audio focus was lost
- Fixed issue where the app would crash if a song menu in the genre UI was opened
- Fixed issue where the artist name would not be shown in the OS audio switcher menu
- Fixed issue where the search view would not update if the library changed
- Fixed visual bug with transitions in the black theme
#### What's Changed
- Ignore MediaStore tags is now on by default
- Ignore MediaStore tags is now Auxio's default and unchangeable behavior. The option has been removed.
- Removed the "Play from genre" option in the library/detail playback mode settings+
- "Use alternate notification action" is now "Custom notification action"
- "Show covers" and "Ignore MediaStore covers" have been unified into "Album covers"

View file

@ -21,7 +21,7 @@
## About
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 a much better listening experience compared to other apps that use the native MediaPlayer API. 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, **It plays music.**
I primarily built Auxio for myself, but you can use it too, I guess.
@ -46,32 +46,25 @@ I primarily built Auxio for myself, but you can use it too, I guess.
- Snappy UI derived from the latest Material Design guidelines
- Opinionated UX that prioritizes ease of use over edge cases
- Customizable behavior
- Advanced media indexer that prioritizes correct metadata
- Precise/Original Dates, Sort Tags, and Release Type support (Experimental)
- Seamless artist system that unifies album artist and artist tags
- Advanced media indexer with support for multiple artists, release types,
precise/original dates, sort tags, and more
- SD Card-aware folder management
- Reliable playback state persistence
- Full ReplayGain support (On MP3, MP4, FLAC, OGG, and OPUS)
- External equalizer support (ex. Wavelet)
- Edge-to-edge
- Embedded covers support
- Search Functionality
- Search functionality
- Headset autoplay
- Stylish widgets that automatically adapt to their size
- Completely private and offline
- No rounded album covers (Unless you want them. Then you can.)
## To come in the future:
- Playlists
- Liked songs
- Artist Images
- More customization options
- Other things, probably
## Permissions
- Storage (`READ_EXTERNAL_STORAGE`): to read and play your media files
- Services (`FOREGROUND_SERVICE`, `WAKE_LOCK`): to keep the music playing even if the app itself is in background
- Storage (`READ_MEDIA_AUDIO`, `READ_EXTERNAL_STORAGE`) to read and play your media files
- Services (`FOREGROUND_SERVICE`, `WAKE_LOCK`) to keep the music playing even if the app itself is in background
## Building

View file

@ -54,7 +54,6 @@ android {
}
}
dependencies {
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"

View file

@ -44,6 +44,10 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
*
* TODO: Add multi-select
*
* TODO: Use proper material attributes (Not the weird dimen attributes I currently have)
*
* TODO: Test out material animation system
*
* @author OxygenCobalt
*/
class MainActivity : AppCompatActivity() {

View file

@ -236,9 +236,10 @@ class MainFragment :
private fun handleExplorePicker(items: List<Artist>?) {
if (items != null) {
navModel.mainNavigateTo(MainNavigationAction.Directions(
MainFragmentDirections.actionPickNavigationArtist(items.map { it.uid }.toTypedArray())
))
navModel.mainNavigateTo(
MainNavigationAction.Directions(
MainFragmentDirections.actionPickNavigationArtist(
items.map { it.uid }.toTypedArray())))
navModel.finishExploreNavigation()
}
}
@ -253,9 +254,9 @@ class MainFragment :
private fun handlePlaybackPicker(song: Song?) {
if (song != null) {
navModel.mainNavigateTo(MainNavigationAction.Directions(
MainFragmentDirections.actionPickPlaybackArtist(song.uid)
))
navModel.mainNavigateTo(
MainNavigationAction.Directions(
MainFragmentDirections.actionPickPlaybackArtist(song.uid)))
playbackModel.finishPlaybackArtistPicker()
}
}

View file

@ -192,12 +192,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
binding.songName.text = item.resolveName(binding.context)
binding.songDuration.text = item.durationMs.formatDurationMs(false)
// binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnLongClickListener {
listener.onOpenMenu(item, it)
true
}
binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnClickListener { listener.onItemClick(item) }
}

View file

@ -160,12 +160,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
binding.parentName.text = item.resolveName(binding.context)
binding.parentInfo.text =
item.date?.resolveDate(binding.context) ?: binding.context.getString(R.string.def_date)
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnLongClickListener {
listener.onOpenMenu(item, it)
true
}
binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnClickListener { listener.onItemClick(item) }
}
@ -194,11 +189,7 @@ private class ArtistSongViewHolder private constructor(private val binding: Item
binding.songAlbumCover.bind(item)
binding.songName.text = item.resolveName(binding.context)
binding.songInfo.text = item.album.resolveName(binding.context)
// binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnLongClickListener {
listener.onOpenMenu(item, it)
true
}
binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnClickListener { listener.onItemClick(item) }
}

View file

@ -80,7 +80,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
// lifecycleObject builds this in the creation step, so doing this is okay.
private val storagePermissionLauncher: ActivityResultLauncher<String> by lifecycleObject {
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
musicModel.reindex()
musicModel.reindex(true)
}
}
@ -321,7 +321,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homeIndexingAction.apply {
visibility = View.VISIBLE
text = context.getString(R.string.lbl_retry)
setOnClickListener { musicModel.reindex() }
setOnClickListener { musicModel.reindex(true) }
}
}
is Indexer.Response.NoMusic -> {
@ -330,7 +330,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homeIndexingAction.apply {
visibility = View.VISIBLE
text = context.getString(R.string.lbl_retry)
setOnClickListener { musicModel.reindex() }
setOnClickListener { musicModel.reindex(true) }
}
}
is Indexer.Response.NoPerms -> {

View file

@ -32,7 +32,8 @@ import org.oxycblt.auxio.music.Song
*
* Pretty much each service component needs to load bitmaps of some kind, but doing a blind image
* request with some target callbacks could result in overlapping requests causing incorrect
* updates. This class (to an extent) resolves this by adding several guards
* updates. This class (to an extent) resolves this by adding a concurrency guard to the image
* callbacks.
*
* @author OxygenCobalt
*/

View file

@ -284,15 +284,11 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
get() = unlikelyToBeNull(_album)
private val artistMusicBrainzIds = raw.artistMusicBrainzIds.parseMultiValue(settings)
private val artistNames = raw.artistNames.parseMultiValue(settings)
private val artistSortNames = raw.artistSortNames.parseMultiValue(settings)
private val albumArtistMusicBrainzIds = raw.albumArtistMusicBrainzIds.parseMultiValue(settings)
private val albumArtistNames = raw.albumArtistNames.parseMultiValue(settings)
private val albumArtistSortNames = raw.albumArtistSortNames.parseMultiValue(settings)
private val rawArtists =

View file

@ -36,19 +36,16 @@ class MusicViewModel : ViewModel(), Indexer.Callback {
private val _statistics = MutableStateFlow<Statistics?>(null)
/** The current statistics of the music library. */
val statistics: StateFlow<Statistics?> get() = _statistics
val statistics: StateFlow<Statistics?>
get() = _statistics
init {
indexer.registerCallback(this)
}
/** Re-index the music library. */
fun reindex() {
indexer.requestReindex(true)
}
fun rescan() {
indexer.requestReindex(false)
/** Re-index the music library while using the cache. */
fun reindex(ignoreCache: Boolean) {
indexer.requestReindex(ignoreCache)
}
override fun onIndexerStateChanged(state: Indexer.State?) {
@ -57,13 +54,13 @@ class MusicViewModel : ViewModel(), Indexer.Callback {
if (state is Indexer.State.Complete && state.response is Indexer.Response.Ok) {
val library = state.response.library
_statistics.value = Statistics(
_statistics.value =
Statistics(
library.songs.size,
library.albums.size,
library.artists.size,
library.genres.size,
library.songs.sumOf { it.durationMs }
)
library.songs.sumOf { it.durationMs })
}
}
@ -71,9 +68,7 @@ class MusicViewModel : ViewModel(), Indexer.Callback {
indexer.unregisterCallback(this)
}
/**
* Non-manipulated statistics about the music library.
*/
/** Non-manipulated statistics about the music library. */
data class Statistics(
/** The amount of songs. */
val songs: Int,
@ -83,6 +78,7 @@ class MusicViewModel : ViewModel(), Indexer.Callback {
val artists: Int,
/** The amount of genres. */
val genres: Int,
/** The total duration of the music library. */
val durationMs: Long
)
}

View file

@ -18,12 +18,7 @@
package org.oxycblt.auxio.music
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.temporal.TemporalQueries
import java.util.Locale
import java.text.SimpleDateFormat
import kotlin.math.max
import kotlin.math.min
import org.oxycblt.auxio.BuildConfig
@ -31,7 +26,6 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.inRangeOrNull
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.nonZeroOrNull
import java.text.SimpleDateFormat
/**
* An ISO-8601/RFC 3339 Date.
@ -92,9 +86,12 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
private fun resolveFullDate(context: Context): String {
return if (month != null) {
// Parse out from an ISO-ish format
val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat)
format.applyPattern("yyyy-MM-dd")
val date = format.parse("$year-$month-${day ?: 1}") ?: return resolveYear(context)
format.applyPattern("yyyy-MM")
val date = format.parse("$year-$month") ?: return resolveYear(context)
// Reformat as a readable month and year
format.applyPattern("MMM yyyy")
format.format(date)
} else {
@ -264,8 +261,7 @@ sealed class ReleaseType {
/**
* Roughly analogous to the MusicBrainz "live" and "remix" secondary types. Unlike the main
* types, these only modify an existing, primary type. They are not implemented for secondary
* types, however they may be expanded to compilations in the future.
* types, these only modify an existing, primary type.
*/
enum class Refinement {
LIVE,
@ -274,7 +270,7 @@ sealed class ReleaseType {
companion object {
// Note: The parsing code is extremely clever in order to reduce duplication. It's
// better just to read the specification behind release types than follow this code.
// better just to read the specification behind release types than to follow this code.
fun parse(types: List<String>): ReleaseType? {
val primary = types.getOrNull(0) ?: return null

View file

@ -70,7 +70,6 @@ class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
}
companion object {
private const val SEPARATOR_COMMA = ','
private const val SEPARATOR_SEMICOLON = ';'
private const val SEPARATOR_SLASH = '/'

View file

@ -19,13 +19,11 @@ package org.oxycblt.auxio.music.picker
import android.os.Bundle
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.collectImmediately
/**
* The [ArtistPickerDialog] for ambiguous artist navigation operations.

View file

@ -24,7 +24,6 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* The [ArtistPickerDialog] for ambiguous artist playback operations.
@ -42,8 +41,6 @@ class ArtistPlaybackPickerDialog : ArtistPickerDialog() {
override fun onItemClick(item: Item) {
super.onItemClick(item)
check(item is Artist) { "Unexpected datatype: ${item::class.simpleName}" }
pickerModel.currentSong.value?.let { song ->
playbackModel.playFromArtist(song, item)
}
pickerModel.currentSong.value?.let { song -> playbackModel.playFromArtist(song, item) }
}
}

View file

@ -1,3 +1,20 @@
/*
* Copyright (c) 2022 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.picker
import androidx.lifecycle.ViewModel
@ -13,10 +30,12 @@ class MusicPickerViewModel : ViewModel(), MusicStore.Callback {
private val musicStore = MusicStore.getInstance()
private val _currentSong = MutableStateFlow<Song?>(null)
val currentSong: StateFlow<Song?> get() = _currentSong
val currentSong: StateFlow<Song?>
get() = _currentSong
private val _currentArtists = MutableStateFlow<List<Artist>?>(null)
val currentArtists: StateFlow<List<Artist>?> get() = _currentArtists
val currentArtists: StateFlow<List<Artist>?>
get() = _currentArtists
fun setSongUid(uid: Music.UID) {
val library = unlikelyToBeNull(musicStore.library)
@ -36,7 +55,7 @@ class MusicPickerViewModel : ViewModel(), MusicStore.Callback {
if (song != null) {
_currentSong.value = library.sanitize(song)
_currentArtists.value = _currentSong.value?.artists
} else if (artists != null){
} else if (artists != null) {
_currentArtists.value = artists.mapNotNull { library.sanitize(it) }
}
}

View file

@ -76,12 +76,14 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
val binding = requireBinding()
binding.aboutSongCount.text = getString(R.string.fmt_lib_song_count, statistics?.songs ?: 0)
requireBinding().aboutAlbumCount.text = getString(R.string.fmt_lib_album_count, statistics?.albums ?: 0)
requireBinding().aboutAlbumCount.text =
getString(R.string.fmt_lib_album_count, statistics?.albums ?: 0)
requireBinding().aboutArtistCount.text =
getString(R.string.fmt_lib_artist_count, statistics?.artists ?: 0)
requireBinding().aboutGenreCount.text = getString(R.string.fmt_lib_genre_count, statistics?.genres ?: 0)
requireBinding().aboutGenreCount.text =
getString(R.string.fmt_lib_genre_count, statistics?.genres ?: 0)
binding.aboutTotalDuration.text =
getString(

View file

@ -120,6 +120,8 @@ class PreferenceFragment : PreferenceFragmentCompat() {
when (preference.key) {
context.getString(R.string.set_key_save_state) -> {
playbackModel.savePlaybackState { saved ->
// Use the nullable context, as we could try to show a toast when this
// fragment is no longer attached.
if (saved) {
this.context?.showToast(R.string.lbl_state_saved)
} else {
@ -144,12 +146,8 @@ class PreferenceFragment : PreferenceFragmentCompat() {
this.context?.showToast(R.string.err_did_not_restore)
}
}
context.getString(R.string.set_key_reindex) -> {
musicModel.reindex()
}
context.getString(R.string.set_key_rescan) -> {
musicModel.rescan()
}
context.getString(R.string.set_key_reindex) -> musicModel.reindex(true)
context.getString(R.string.set_key_rescan) -> musicModel.reindex(false)
else -> return super.onPreferenceTreeClick(preference)
}

View file

@ -42,11 +42,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
binding.songAlbumCover.bind(item)
binding.songName.text = item.resolveName(binding.context)
binding.songInfo.text = item.resolveArtistContents(binding.context)
// binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnLongClickListener {
listener.onOpenMenu(item, it)
true
}
binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnClickListener { listener.onItemClick(item) }
}
@ -79,11 +75,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
binding.parentImage.bind(item)
binding.parentName.text = item.resolveName(binding.context)
binding.parentInfo.text = item.resolveArtistContents(binding.context)
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnLongClickListener {
listener.onOpenMenu(item, it)
true
}
binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnClickListener { listener.onItemClick(item) }
}
@ -129,11 +121,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size)
}
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnLongClickListener {
listener.onOpenMenu(item, it)
true
}
binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnClickListener { listener.onItemClick(item) }
}
@ -172,11 +160,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
R.string.fmt_two,
binding.context.getPlural(R.plurals.fmt_artist_count, item.artists.size),
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnLongClickListener {
listener.onOpenMenu(item, it)
true
}
binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnClickListener { listener.onItemClick(item) }
}

View file

@ -80,7 +80,6 @@
style="@style/Widget.Auxio.Button.Icon.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:icon="@drawable/ic_more_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -49,7 +49,6 @@
style="@style/Widget.Auxio.Button.Icon.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:icon="@drawable/ic_more_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -49,7 +49,6 @@
style="@style/Widget.Auxio.Button.Icon.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:icon="@drawable/ic_more_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"