ui: re-add more button
Re-enable the more button in preparation for multi-select.
This commit is contained in:
parent
2242c413d8
commit
a3772b65c0
23 changed files with 87 additions and 122 deletions
|
@ -30,13 +30,12 @@
|
||||||
- Fixed issue where the scroll popup would not display correctly in landscape mode [#230]
|
- 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
|
- Fixed issue where the playback progress would continue in the notification when
|
||||||
audio focus was lost
|
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 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 issue where the search view would not update if the library changed
|
||||||
- Fixed visual bug with transitions in the black theme
|
- Fixed visual bug with transitions in the black theme
|
||||||
|
|
||||||
#### What's Changed
|
#### 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+
|
- Removed the "Play from genre" option in the library/detail playback mode settings+
|
||||||
- "Use alternate notification action" is now "Custom notification action"
|
- "Use alternate notification action" is now "Custom notification action"
|
||||||
- "Show covers" and "Ignore MediaStore covers" have been unified into "Album covers"
|
- "Show covers" and "Ignore MediaStore covers" have been unified into "Album covers"
|
||||||
|
|
21
README.md
21
README.md
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
## About
|
## 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.
|
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
|
- Snappy UI derived from the latest Material Design guidelines
|
||||||
- Opinionated UX that prioritizes ease of use over edge cases
|
- Opinionated UX that prioritizes ease of use over edge cases
|
||||||
- Customizable behavior
|
- Customizable behavior
|
||||||
- Advanced media indexer that prioritizes correct metadata
|
- Seamless artist system that unifies album artist and artist tags
|
||||||
- Precise/Original Dates, Sort Tags, and Release Type support (Experimental)
|
- Advanced media indexer with support for multiple artists, release types,
|
||||||
|
precise/original dates, sort tags, and more
|
||||||
- SD Card-aware folder management
|
- SD Card-aware folder management
|
||||||
- Reliable playback state persistence
|
- Reliable playback state persistence
|
||||||
- Full ReplayGain support (On MP3, MP4, FLAC, OGG, and OPUS)
|
- Full ReplayGain support (On MP3, MP4, FLAC, OGG, and OPUS)
|
||||||
- External equalizer support (ex. Wavelet)
|
- External equalizer support (ex. Wavelet)
|
||||||
- Edge-to-edge
|
- Edge-to-edge
|
||||||
- Embedded covers support
|
- Embedded covers support
|
||||||
- Search Functionality
|
- Search functionality
|
||||||
- Headset autoplay
|
- Headset autoplay
|
||||||
- Stylish widgets that automatically adapt to their size
|
- Stylish widgets that automatically adapt to their size
|
||||||
- Completely private and offline
|
- Completely private and offline
|
||||||
- No rounded album covers (Unless you want them. Then you can.)
|
- 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
|
## Permissions
|
||||||
|
|
||||||
- Storage (`READ_EXTERNAL_STORAGE`): to read and play your media files
|
- 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
|
- Services (`FOREGROUND_SERVICE`, `WAKE_LOCK`) to keep the music playing even if the app itself is in background
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,6 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Kotlin
|
// Kotlin
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
|
@ -44,6 +44,10 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
*
|
*
|
||||||
* TODO: Add multi-select
|
* 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
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
|
@ -236,9 +236,10 @@ class MainFragment :
|
||||||
|
|
||||||
private fun handleExplorePicker(items: List<Artist>?) {
|
private fun handleExplorePicker(items: List<Artist>?) {
|
||||||
if (items != null) {
|
if (items != null) {
|
||||||
navModel.mainNavigateTo(MainNavigationAction.Directions(
|
navModel.mainNavigateTo(
|
||||||
MainFragmentDirections.actionPickNavigationArtist(items.map { it.uid }.toTypedArray())
|
MainNavigationAction.Directions(
|
||||||
))
|
MainFragmentDirections.actionPickNavigationArtist(
|
||||||
|
items.map { it.uid }.toTypedArray())))
|
||||||
navModel.finishExploreNavigation()
|
navModel.finishExploreNavigation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -253,9 +254,9 @@ class MainFragment :
|
||||||
|
|
||||||
private fun handlePlaybackPicker(song: Song?) {
|
private fun handlePlaybackPicker(song: Song?) {
|
||||||
if (song != null) {
|
if (song != null) {
|
||||||
navModel.mainNavigateTo(MainNavigationAction.Directions(
|
navModel.mainNavigateTo(
|
||||||
MainFragmentDirections.actionPickPlaybackArtist(song.uid)
|
MainNavigationAction.Directions(
|
||||||
))
|
MainFragmentDirections.actionPickPlaybackArtist(song.uid)))
|
||||||
playbackModel.finishPlaybackArtistPicker()
|
playbackModel.finishPlaybackArtistPicker()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -192,12 +192,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
|
||||||
|
|
||||||
binding.songName.text = item.resolveName(binding.context)
|
binding.songName.text = item.resolveName(binding.context)
|
||||||
binding.songDuration.text = item.durationMs.formatDurationMs(false)
|
binding.songDuration.text = item.durationMs.formatDurationMs(false)
|
||||||
|
binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||||
// binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
|
||||||
binding.root.setOnLongClickListener {
|
|
||||||
listener.onOpenMenu(item, it)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
binding.root.setOnClickListener { listener.onItemClick(item) }
|
binding.root.setOnClickListener { listener.onItemClick(item) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -160,12 +160,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
|
||||||
binding.parentName.text = item.resolveName(binding.context)
|
binding.parentName.text = item.resolveName(binding.context)
|
||||||
binding.parentInfo.text =
|
binding.parentInfo.text =
|
||||||
item.date?.resolveDate(binding.context) ?: binding.context.getString(R.string.def_date)
|
item.date?.resolveDate(binding.context) ?: binding.context.getString(R.string.def_date)
|
||||||
|
binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||||
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
|
||||||
binding.root.setOnLongClickListener {
|
|
||||||
listener.onOpenMenu(item, it)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
binding.root.setOnClickListener { listener.onItemClick(item) }
|
binding.root.setOnClickListener { listener.onItemClick(item) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,11 +189,7 @@ private class ArtistSongViewHolder private constructor(private val binding: Item
|
||||||
binding.songAlbumCover.bind(item)
|
binding.songAlbumCover.bind(item)
|
||||||
binding.songName.text = item.resolveName(binding.context)
|
binding.songName.text = item.resolveName(binding.context)
|
||||||
binding.songInfo.text = item.album.resolveName(binding.context)
|
binding.songInfo.text = item.album.resolveName(binding.context)
|
||||||
// binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||||
binding.root.setOnLongClickListener {
|
|
||||||
listener.onOpenMenu(item, it)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
binding.root.setOnClickListener { listener.onItemClick(item) }
|
binding.root.setOnClickListener { listener.onItemClick(item) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
// lifecycleObject builds this in the creation step, so doing this is okay.
|
// lifecycleObject builds this in the creation step, so doing this is okay.
|
||||||
private val storagePermissionLauncher: ActivityResultLauncher<String> by lifecycleObject {
|
private val storagePermissionLauncher: ActivityResultLauncher<String> by lifecycleObject {
|
||||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||||
musicModel.reindex()
|
musicModel.reindex(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,7 +321,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
binding.homeIndexingAction.apply {
|
binding.homeIndexingAction.apply {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
text = context.getString(R.string.lbl_retry)
|
text = context.getString(R.string.lbl_retry)
|
||||||
setOnClickListener { musicModel.reindex() }
|
setOnClickListener { musicModel.reindex(true) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Indexer.Response.NoMusic -> {
|
is Indexer.Response.NoMusic -> {
|
||||||
|
@ -330,7 +330,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
binding.homeIndexingAction.apply {
|
binding.homeIndexingAction.apply {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
text = context.getString(R.string.lbl_retry)
|
text = context.getString(R.string.lbl_retry)
|
||||||
setOnClickListener { musicModel.reindex() }
|
setOnClickListener { musicModel.reindex(true) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Indexer.Response.NoPerms -> {
|
is Indexer.Response.NoPerms -> {
|
||||||
|
|
|
@ -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
|
* 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
|
* 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
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -284,15 +284,11 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
get() = unlikelyToBeNull(_album)
|
get() = unlikelyToBeNull(_album)
|
||||||
|
|
||||||
private val artistMusicBrainzIds = raw.artistMusicBrainzIds.parseMultiValue(settings)
|
private val artistMusicBrainzIds = raw.artistMusicBrainzIds.parseMultiValue(settings)
|
||||||
|
|
||||||
private val artistNames = raw.artistNames.parseMultiValue(settings)
|
private val artistNames = raw.artistNames.parseMultiValue(settings)
|
||||||
|
|
||||||
private val artistSortNames = raw.artistSortNames.parseMultiValue(settings)
|
private val artistSortNames = raw.artistSortNames.parseMultiValue(settings)
|
||||||
|
|
||||||
private val albumArtistMusicBrainzIds = raw.albumArtistMusicBrainzIds.parseMultiValue(settings)
|
private val albumArtistMusicBrainzIds = raw.albumArtistMusicBrainzIds.parseMultiValue(settings)
|
||||||
|
|
||||||
private val albumArtistNames = raw.albumArtistNames.parseMultiValue(settings)
|
private val albumArtistNames = raw.albumArtistNames.parseMultiValue(settings)
|
||||||
|
|
||||||
private val albumArtistSortNames = raw.albumArtistSortNames.parseMultiValue(settings)
|
private val albumArtistSortNames = raw.albumArtistSortNames.parseMultiValue(settings)
|
||||||
|
|
||||||
private val rawArtists =
|
private val rawArtists =
|
||||||
|
|
|
@ -36,19 +36,16 @@ class MusicViewModel : ViewModel(), Indexer.Callback {
|
||||||
|
|
||||||
private val _statistics = MutableStateFlow<Statistics?>(null)
|
private val _statistics = MutableStateFlow<Statistics?>(null)
|
||||||
/** The current statistics of the music library. */
|
/** The current statistics of the music library. */
|
||||||
val statistics: StateFlow<Statistics?> get() = _statistics
|
val statistics: StateFlow<Statistics?>
|
||||||
|
get() = _statistics
|
||||||
|
|
||||||
init {
|
init {
|
||||||
indexer.registerCallback(this)
|
indexer.registerCallback(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Re-index the music library. */
|
/** Re-index the music library while using the cache. */
|
||||||
fun reindex() {
|
fun reindex(ignoreCache: Boolean) {
|
||||||
indexer.requestReindex(true)
|
indexer.requestReindex(ignoreCache)
|
||||||
}
|
|
||||||
|
|
||||||
fun rescan() {
|
|
||||||
indexer.requestReindex(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIndexerStateChanged(state: Indexer.State?) {
|
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) {
|
if (state is Indexer.State.Complete && state.response is Indexer.Response.Ok) {
|
||||||
val library = state.response.library
|
val library = state.response.library
|
||||||
_statistics.value = Statistics(
|
_statistics.value =
|
||||||
library.songs.size,
|
Statistics(
|
||||||
library.albums.size,
|
library.songs.size,
|
||||||
library.artists.size,
|
library.albums.size,
|
||||||
library.genres.size,
|
library.artists.size,
|
||||||
library.songs.sumOf { it.durationMs }
|
library.genres.size,
|
||||||
)
|
library.songs.sumOf { it.durationMs })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,9 +68,7 @@ class MusicViewModel : ViewModel(), Indexer.Callback {
|
||||||
indexer.unregisterCallback(this)
|
indexer.unregisterCallback(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Non-manipulated statistics about the music library. */
|
||||||
* Non-manipulated statistics about the music library.
|
|
||||||
*/
|
|
||||||
data class Statistics(
|
data class Statistics(
|
||||||
/** The amount of songs. */
|
/** The amount of songs. */
|
||||||
val songs: Int,
|
val songs: Int,
|
||||||
|
@ -83,6 +78,7 @@ class MusicViewModel : ViewModel(), Indexer.Callback {
|
||||||
val artists: Int,
|
val artists: Int,
|
||||||
/** The amount of genres. */
|
/** The amount of genres. */
|
||||||
val genres: Int,
|
val genres: Int,
|
||||||
|
/** The total duration of the music library. */
|
||||||
val durationMs: Long
|
val durationMs: Long
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,12 +18,7 @@
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import java.text.SimpleDateFormat
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import java.time.ZoneId
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import java.time.temporal.TemporalQueries
|
|
||||||
import java.util.Locale
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import org.oxycblt.auxio.BuildConfig
|
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.inRangeOrNull
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An ISO-8601/RFC 3339 Date.
|
* 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 {
|
private fun resolveFullDate(context: Context): String {
|
||||||
return if (month != null) {
|
return if (month != null) {
|
||||||
|
// Parse out from an ISO-ish format
|
||||||
val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat)
|
val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat)
|
||||||
format.applyPattern("yyyy-MM-dd")
|
format.applyPattern("yyyy-MM")
|
||||||
val date = format.parse("$year-$month-${day ?: 1}") ?: return resolveYear(context)
|
val date = format.parse("$year-$month") ?: return resolveYear(context)
|
||||||
|
|
||||||
|
// Reformat as a readable month and year
|
||||||
format.applyPattern("MMM yyyy")
|
format.applyPattern("MMM yyyy")
|
||||||
format.format(date)
|
format.format(date)
|
||||||
} else {
|
} else {
|
||||||
|
@ -264,8 +261,7 @@ sealed class ReleaseType {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Roughly analogous to the MusicBrainz "live" and "remix" secondary types. Unlike the main
|
* 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, these only modify an existing, primary type.
|
||||||
* types, however they may be expanded to compilations in the future.
|
|
||||||
*/
|
*/
|
||||||
enum class Refinement {
|
enum class Refinement {
|
||||||
LIVE,
|
LIVE,
|
||||||
|
@ -274,7 +270,7 @@ sealed class ReleaseType {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// Note: The parsing code is extremely clever in order to reduce duplication. It's
|
// 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? {
|
fun parse(types: List<String>): ReleaseType? {
|
||||||
val primary = types.getOrNull(0) ?: return null
|
val primary = types.getOrNull(0) ?: return null
|
||||||
|
|
|
@ -70,7 +70,6 @@ class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val SEPARATOR_COMMA = ','
|
private const val SEPARATOR_COMMA = ','
|
||||||
private const val SEPARATOR_SEMICOLON = ';'
|
private const val SEPARATOR_SEMICOLON = ';'
|
||||||
private const val SEPARATOR_SLASH = '/'
|
private const val SEPARATOR_SLASH = '/'
|
||||||
|
|
|
@ -19,13 +19,11 @@ 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.findNavController
|
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
|
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [ArtistPickerDialog] for ambiguous artist navigation operations.
|
* The [ArtistPickerDialog] for ambiguous artist navigation operations.
|
||||||
|
|
|
@ -24,7 +24,6 @@ import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [ArtistPickerDialog] for ambiguous artist playback operations.
|
* The [ArtistPickerDialog] for ambiguous artist playback operations.
|
||||||
|
@ -42,8 +41,6 @@ class ArtistPlaybackPickerDialog : ArtistPickerDialog() {
|
||||||
override fun onItemClick(item: Item) {
|
override fun onItemClick(item: Item) {
|
||||||
super.onItemClick(item)
|
super.onItemClick(item)
|
||||||
check(item is Artist) { "Unexpected datatype: ${item::class.simpleName}" }
|
check(item is Artist) { "Unexpected datatype: ${item::class.simpleName}" }
|
||||||
pickerModel.currentSong.value?.let { song ->
|
pickerModel.currentSong.value?.let { song -> playbackModel.playFromArtist(song, item) }
|
||||||
playbackModel.playFromArtist(song, item)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
package org.oxycblt.auxio.music.picker
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
@ -13,15 +30,17 @@ class MusicPickerViewModel : ViewModel(), MusicStore.Callback {
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
|
|
||||||
private val _currentSong = MutableStateFlow<Song?>(null)
|
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)
|
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) {
|
fun setSongUid(uid: Music.UID) {
|
||||||
val library = unlikelyToBeNull(musicStore.library)
|
val library = unlikelyToBeNull(musicStore.library)
|
||||||
_currentSong.value = library.find(uid)
|
_currentSong.value = library.find(uid)
|
||||||
_currentArtists.value = _currentSong.value?.artists
|
_currentArtists.value = _currentSong.value?.artists
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setArtistUids(uids: Array<Music.UID>) {
|
fun setArtistUids(uids: Array<Music.UID>) {
|
||||||
|
@ -36,9 +55,9 @@ class MusicPickerViewModel : ViewModel(), MusicStore.Callback {
|
||||||
if (song != null) {
|
if (song != null) {
|
||||||
_currentSong.value = library.sanitize(song)
|
_currentSong.value = library.sanitize(song)
|
||||||
_currentArtists.value = _currentSong.value?.artists
|
_currentArtists.value = _currentSong.value?.artists
|
||||||
} else if (artists != null){
|
} else if (artists != null) {
|
||||||
_currentArtists.value = artists.mapNotNull { library.sanitize(it) }
|
_currentArtists.value = artists.mapNotNull { library.sanitize(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,7 @@ class PlaybackViewModel(application: Application) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Complete the picker opening process when playing from an artist. */
|
/** Complete the picker opening process when playing from an artist. */
|
||||||
fun finishPlaybackArtistPicker() {
|
fun finishPlaybackArtistPicker() {
|
||||||
_artistPlaybackPickerSong.value = null
|
_artistPlaybackPickerSong.value = null
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,12 +76,14 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
binding.aboutSongCount.text = getString(R.string.fmt_lib_song_count, statistics?.songs ?: 0)
|
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 =
|
requireBinding().aboutArtistCount.text =
|
||||||
getString(R.string.fmt_lib_artist_count, statistics?.artists ?: 0)
|
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 =
|
binding.aboutTotalDuration.text =
|
||||||
getString(
|
getString(
|
||||||
|
|
|
@ -120,6 +120,8 @@ class PreferenceFragment : PreferenceFragmentCompat() {
|
||||||
when (preference.key) {
|
when (preference.key) {
|
||||||
context.getString(R.string.set_key_save_state) -> {
|
context.getString(R.string.set_key_save_state) -> {
|
||||||
playbackModel.savePlaybackState { saved ->
|
playbackModel.savePlaybackState { saved ->
|
||||||
|
// Use the nullable context, as we could try to show a toast when this
|
||||||
|
// fragment is no longer attached.
|
||||||
if (saved) {
|
if (saved) {
|
||||||
this.context?.showToast(R.string.lbl_state_saved)
|
this.context?.showToast(R.string.lbl_state_saved)
|
||||||
} else {
|
} else {
|
||||||
|
@ -144,12 +146,8 @@ class PreferenceFragment : PreferenceFragmentCompat() {
|
||||||
this.context?.showToast(R.string.err_did_not_restore)
|
this.context?.showToast(R.string.err_did_not_restore)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
context.getString(R.string.set_key_reindex) -> {
|
context.getString(R.string.set_key_reindex) -> musicModel.reindex(true)
|
||||||
musicModel.reindex()
|
context.getString(R.string.set_key_rescan) -> musicModel.reindex(false)
|
||||||
}
|
|
||||||
context.getString(R.string.set_key_rescan) -> {
|
|
||||||
musicModel.rescan()
|
|
||||||
}
|
|
||||||
else -> return super.onPreferenceTreeClick(preference)
|
else -> return super.onPreferenceTreeClick(preference)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,11 +42,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||||
binding.songAlbumCover.bind(item)
|
binding.songAlbumCover.bind(item)
|
||||||
binding.songName.text = item.resolveName(binding.context)
|
binding.songName.text = item.resolveName(binding.context)
|
||||||
binding.songInfo.text = item.resolveArtistContents(binding.context)
|
binding.songInfo.text = item.resolveArtistContents(binding.context)
|
||||||
// binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||||
binding.root.setOnLongClickListener {
|
|
||||||
listener.onOpenMenu(item, it)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
binding.root.setOnClickListener { listener.onItemClick(item) }
|
binding.root.setOnClickListener { listener.onItemClick(item) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,11 +75,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
|
||||||
binding.parentImage.bind(item)
|
binding.parentImage.bind(item)
|
||||||
binding.parentName.text = item.resolveName(binding.context)
|
binding.parentName.text = item.resolveName(binding.context)
|
||||||
binding.parentInfo.text = item.resolveArtistContents(binding.context)
|
binding.parentInfo.text = item.resolveArtistContents(binding.context)
|
||||||
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||||
binding.root.setOnLongClickListener {
|
|
||||||
listener.onOpenMenu(item, it)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
binding.root.setOnClickListener { listener.onItemClick(item) }
|
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.context.getPlural(R.plurals.fmt_album_count, item.albums.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||||
binding.root.setOnLongClickListener {
|
|
||||||
listener.onOpenMenu(item, it)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
binding.root.setOnClickListener { listener.onItemClick(item) }
|
binding.root.setOnClickListener { listener.onItemClick(item) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,11 +160,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
|
||||||
R.string.fmt_two,
|
R.string.fmt_two,
|
||||||
binding.context.getPlural(R.plurals.fmt_artist_count, item.artists.size),
|
binding.context.getPlural(R.plurals.fmt_artist_count, item.artists.size),
|
||||||
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
|
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
|
||||||
// binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||||
binding.root.setOnLongClickListener {
|
|
||||||
listener.onOpenMenu(item, it)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
binding.root.setOnClickListener { listener.onItemClick(item) }
|
binding.root.setOnClickListener { listener.onItemClick(item) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,6 @@
|
||||||
style="@style/Widget.Auxio.Button.Icon.Small"
|
style="@style/Widget.Auxio.Button.Icon.Small"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:visibility="gone"
|
|
||||||
app:icon="@drawable/ic_more_24"
|
app:icon="@drawable/ic_more_24"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
|
|
@ -49,7 +49,6 @@
|
||||||
style="@style/Widget.Auxio.Button.Icon.Small"
|
style="@style/Widget.Auxio.Button.Icon.Small"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:visibility="gone"
|
|
||||||
app:icon="@drawable/ic_more_24"
|
app:icon="@drawable/ic_more_24"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
|
|
@ -49,7 +49,6 @@
|
||||||
style="@style/Widget.Auxio.Button.Icon.Small"
|
style="@style/Widget.Auxio.Button.Icon.Small"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:visibility="gone"
|
|
||||||
app:icon="@drawable/ic_more_24"
|
app:icon="@drawable/ic_more_24"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
|
Loading…
Reference in a new issue