Merge branch 'dev' into feature/cover_carousel
This commit is contained in:
commit
c838813434
190 changed files with 3926 additions and 2510 deletions
6
.github/CONTRIBUTING.md
vendored
6
.github/CONTRIBUTING.md
vendored
|
|
@ -39,8 +39,8 @@ If you have knowledge of Android/Kotlin, feel free to to contribute to the proje
|
||||||
- If you want to help out with an existing bug report, comment on the issue that you want to fix saying that you are going to try your hand at it.
|
- If you want to help out with an existing bug report, comment on the issue that you want to fix saying that you are going to try your hand at it.
|
||||||
- If you want to add something, its recommended to open up an issue for what you want to change before you start working on it. That way I can determine if the addition will be merged in the first place, and generally gives a heads-up overall.
|
- If you want to add something, its recommended to open up an issue for what you want to change before you start working on it. That way I can determine if the addition will be merged in the first place, and generally gives a heads-up overall.
|
||||||
- Do not bring non-free software into the project, such as Binary Blobs.
|
- Do not bring non-free software into the project, such as Binary Blobs.
|
||||||
- Stick to [F-Droid Including Guidelines](https://f-droid.org/wiki/page/Inclusion_Policy)
|
- Stick to [F-Droid Inclusion Guidelines](https://f-droid.org/wiki/page/Inclusion_Policy)
|
||||||
- Make sure you stick to Auxio's styling with [ktlint](https://github.com/pinterest/ktlint). `ktlintformat` should run on every build.
|
- Make sure you stick to Auxio's styling, which should be auto-formatted on every build.
|
||||||
- Please ***FULLY TEST*** your changes before creating a PR. Untested code will not be merged.
|
- Please ***FULLY TEST*** your changes before creating a PR. Untested code will not be merged.
|
||||||
- Java code will **NOT** be accepted. Kotlin only.
|
- Only **Kotlin** will be accepted, except for the case that a UI component must be vendored in the project.
|
||||||
- Keep your code up the date with the upstream and continue to maintain it after you create the PR. This makes it less of a hassle to merge.
|
- Keep your code up the date with the upstream and continue to maintain it after you create the PR. This makes it less of a hassle to merge.
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,18 @@
|
||||||
## dev
|
## dev
|
||||||
|
|
||||||
#### What's New
|
#### What's New
|
||||||
- Menus have been refreshed with a cleaner look
|
- Item and sort menus have been refreshed with a cleaner look
|
||||||
|
- Added ability to sort playlists
|
||||||
|
- Added option to play song by itself in library/item details
|
||||||
|
|
||||||
#### What's Improved
|
#### What's Improved
|
||||||
- Made "Add to Playlist" action more prominent in selection toolbar
|
- Made "Add to Playlist" action more prominent in selection toolbar
|
||||||
- Fixed notification album covers not updating after changing the cover
|
- Fixed notification album covers not updating after changing the cover
|
||||||
aspect ratio setting
|
aspect ratio setting
|
||||||
|
|
||||||
|
#### What's Fixed
|
||||||
|
- Playlist detail view now respects playback settings
|
||||||
|
|
||||||
#### Dev/Meta
|
#### Dev/Meta
|
||||||
- Unified navigation graph
|
- Unified navigation graph
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 [ExoPlayer](https://exoplayer.dev/), 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 modern media playback libraries, 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.
|
||||||
|
|
||||||
|
|
@ -42,7 +42,7 @@ I primarily built Auxio for myself, but you can use it too, I guess.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [ExoPlayer](https://exoplayer.dev/)-based playback
|
- Playback based on [Media3 ExoPlayer](https://developer.android.com/guide/topics/media/exoplayer)
|
||||||
- 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
|
||||||
|
|
@ -69,12 +69,11 @@ precise/original dates, sort tags, and more
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
Auxio relies on a custom version of ExoPlayer that enables some extra features. This adds some caveats to
|
Auxio relies on a custom version of Media3 that enables some extra features. This adds some caveats to the build process:
|
||||||
the build process:
|
|
||||||
1. `cmake` and `ninja-build` must be installed before building the project.
|
1. `cmake` and `ninja-build` must be installed before building the project.
|
||||||
2. The project uses submodules, so when cloning initially, use `git clone --recurse-submodules` to properly
|
2. The project uses submodules, so when cloning initially, use `git clone --recurse-submodules` to properly
|
||||||
download the external code.
|
download the external code.
|
||||||
3. You are **unable** to build this project on windows, as the custom ExoPlayer build runs shell scripts that
|
3. You are **unable** to build this project on windows, as the custom Media3 build runs shell scripts that
|
||||||
will only work on unix-based systems.
|
will only work on unix-based systems.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ plugins {
|
||||||
id "kotlin-parcelize"
|
id "kotlin-parcelize"
|
||||||
id "dagger.hilt.android.plugin"
|
id "dagger.hilt.android.plugin"
|
||||||
id "kotlin-kapt"
|
id "kotlin-kapt"
|
||||||
id 'org.jetbrains.kotlin.android'
|
id "com.google.devtools.ksp"
|
||||||
|
id "org.jetbrains.kotlin.android"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -77,7 +78,7 @@ dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
|
||||||
def coroutines_version = '1.7.1'
|
def coroutines_version = '1.7.2'
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$coroutines_version"
|
||||||
|
|
||||||
|
|
@ -118,7 +119,9 @@ dependencies {
|
||||||
// Database
|
// Database
|
||||||
def room_version = '2.6.0-alpha02'
|
def room_version = '2.6.0-alpha02'
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
kapt "androidx.room:room-compiler:$room_version"
|
// I have no clue why, but using KSP breaks the playlist database definition.
|
||||||
|
//noinspection KaptUsageInsteadOfKsp
|
||||||
|
ksp "androidx.room:room-compiler:$room_version"
|
||||||
implementation "androidx.room:room-ktx:$room_version"
|
implementation "androidx.room:room-ktx:$room_version"
|
||||||
|
|
||||||
// --- THIRD PARTY ---
|
// --- THIRD PARTY ---
|
||||||
|
|
@ -142,7 +145,7 @@ dependencies {
|
||||||
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
|
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
|
||||||
|
|
||||||
// Testing
|
// Testing
|
||||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.11'
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
|
||||||
testImplementation "junit:junit:4.13.2"
|
testImplementation "junit:junit:4.13.2"
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
|
|
|
||||||
|
|
@ -65,14 +65,14 @@ object IntegerTable {
|
||||||
const val REPEAT_MODE_ALL = 0xA101
|
const val REPEAT_MODE_ALL = 0xA101
|
||||||
/** RepeatMode.TRACK */
|
/** RepeatMode.TRACK */
|
||||||
const val REPEAT_MODE_TRACK = 0xA102
|
const val REPEAT_MODE_TRACK = 0xA102
|
||||||
/** PlaybackMode.IN_GENRE */
|
// /** PlaybackMode.IN_GENRE (No longer used but still reserved) */
|
||||||
const val PLAYBACK_MODE_IN_GENRE = 0xA103
|
// const val PLAYBACK_MODE_IN_GENRE = 0xA103
|
||||||
/** PlaybackMode.IN_ARTIST */
|
// /** PlaybackMode.IN_ARTIST (No longer used but still reserved) */
|
||||||
const val PLAYBACK_MODE_IN_ARTIST = 0xA104
|
// const val PLAYBACK_MODE_IN_ARTIST = 0xA104
|
||||||
/** PlaybackMode.IN_ALBUM */
|
// /** PlaybackMode.IN_ALBUM (No longer used but still reserved) */
|
||||||
const val PLAYBACK_MODE_IN_ALBUM = 0xA105
|
// const val PLAYBACK_MODE_IN_ALBUM = 0xA105
|
||||||
/** PlaybackMode.ALL_SONGS */
|
// /** PlaybackMode.ALL_SONGS (No longer used but still reserved) */
|
||||||
const val PLAYBACK_MODE_ALL_SONGS = 0xA106
|
// const val PLAYBACK_MODE_ALL_SONGS = 0xA106
|
||||||
/** MusicMode.SONGS */
|
/** MusicMode.SONGS */
|
||||||
const val MUSIC_MODE_SONGS = 0xA10B
|
const val MUSIC_MODE_SONGS = 0xA10B
|
||||||
/** MusicMode.ALBUMS */
|
/** MusicMode.ALBUMS */
|
||||||
|
|
@ -101,8 +101,6 @@ object IntegerTable {
|
||||||
const val SORT_BY_TRACK = 0xA117
|
const val SORT_BY_TRACK = 0xA117
|
||||||
/** Sort.Mode.ByDateAdded */
|
/** Sort.Mode.ByDateAdded */
|
||||||
const val SORT_BY_DATE_ADDED = 0xA118
|
const val SORT_BY_DATE_ADDED = 0xA118
|
||||||
/** Sort.Mode.None */
|
|
||||||
const val SORT_BY_NONE = 0xA11F
|
|
||||||
/** ReplayGainMode.Off (No longer used but still reserved) */
|
/** ReplayGainMode.Off (No longer used but still reserved) */
|
||||||
// const val REPLAY_GAIN_MODE_OFF = 0xA110
|
// const val REPLAY_GAIN_MODE_OFF = 0xA110
|
||||||
/** ReplayGainMode.Track */
|
/** ReplayGainMode.Track */
|
||||||
|
|
@ -123,4 +121,16 @@ object IntegerTable {
|
||||||
const val COVER_MODE_MEDIA_STORE = 0xA11D
|
const val COVER_MODE_MEDIA_STORE = 0xA11D
|
||||||
/** CoverMode.Quality */
|
/** CoverMode.Quality */
|
||||||
const val COVER_MODE_QUALITY = 0xA11E
|
const val COVER_MODE_QUALITY = 0xA11E
|
||||||
|
/** PlaySong.FromAll */
|
||||||
|
const val PLAY_SONG_FROM_ALL = 0xA11F
|
||||||
|
/** PlaySong.FromAlbum */
|
||||||
|
const val PLAY_SONG_FROM_ALBUM = 0xA120
|
||||||
|
/** PlaySong.FromArtist */
|
||||||
|
const val PLAY_SONG_FROM_ARTIST = 0xA121
|
||||||
|
/** PlaySong.FromGenre */
|
||||||
|
const val PLAY_SONG_FROM_GENRE = 0xA122
|
||||||
|
/** PlaySong.FromPlaylist */
|
||||||
|
const val PLAY_SONG_FROM_PLAYLIST = 0xA123
|
||||||
|
/** PlaySong.ByItself */
|
||||||
|
const val PLAY_SONG_BY_ITSELF = 0xA124
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,19 +26,22 @@ import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.FragmentContainerView
|
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavDestination
|
import androidx.navigation.NavDestination
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.google.android.material.R as MR
|
import com.google.android.material.R as MR
|
||||||
import com.google.android.material.bottomsheet.BackportBottomSheetBehavior
|
import com.google.android.material.bottomsheet.BackportBottomSheetBehavior
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
|
import com.google.android.material.transition.MaterialFadeThrough
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
||||||
import org.oxycblt.auxio.detail.DetailViewModel
|
import org.oxycblt.auxio.detail.DetailViewModel
|
||||||
|
import org.oxycblt.auxio.home.HomeViewModel
|
||||||
|
import org.oxycblt.auxio.home.Outer
|
||||||
import org.oxycblt.auxio.list.ListViewModel
|
import org.oxycblt.auxio.list.ListViewModel
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
|
@ -53,6 +56,7 @@ import org.oxycblt.auxio.util.coordinatorLayoutBehavior
|
||||||
import org.oxycblt.auxio.util.getAttrColorCompat
|
import org.oxycblt.auxio.util.getAttrColorCompat
|
||||||
import org.oxycblt.auxio.util.getDimen
|
import org.oxycblt.auxio.util.getDimen
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
import org.oxycblt.auxio.util.navigateSafe
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
|
|
@ -66,17 +70,23 @@ class MainFragment :
|
||||||
ViewBindingFragment<FragmentMainBinding>(),
|
ViewBindingFragment<FragmentMainBinding>(),
|
||||||
ViewTreeObserver.OnPreDrawListener,
|
ViewTreeObserver.OnPreDrawListener,
|
||||||
NavController.OnDestinationChangedListener {
|
NavController.OnDestinationChangedListener {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
|
||||||
private val listModel: ListViewModel by activityViewModels()
|
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
|
private val listModel: ListViewModel by activityViewModels()
|
||||||
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private var sheetBackCallback: SheetBackPressedCallback? = null
|
private var sheetBackCallback: SheetBackPressedCallback? = null
|
||||||
private var detailBackCallback: DetailBackPressedCallback? = null
|
private var detailBackCallback: DetailBackPressedCallback? = null
|
||||||
private var selectionBackCallback: SelectionBackPressedCallback? = null
|
private var selectionBackCallback: SelectionBackPressedCallback? = null
|
||||||
private var exploreBackCallback: ExploreBackPressedCallback? = null
|
|
||||||
private var lastInsets: WindowInsets? = null
|
private var lastInsets: WindowInsets? = null
|
||||||
private var elevationNormal = 0f
|
private var elevationNormal = 0f
|
||||||
private var initialNavDestinationChange = true
|
private var initialNavDestinationChange = true
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enterTransition = MaterialFadeThrough()
|
||||||
|
exitTransition = MaterialFadeThrough()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentMainBinding.inflate(inflater)
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentMainBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) {
|
||||||
|
|
@ -92,28 +102,17 @@ class MainFragment :
|
||||||
// Currently all back press callbacks are handled in MainFragment, as it's not guaranteed
|
// Currently all back press callbacks are handled in MainFragment, as it's not guaranteed
|
||||||
// that instantiating these callbacks in their respective fragments would result in the
|
// that instantiating these callbacks in their respective fragments would result in the
|
||||||
// correct order.
|
// correct order.
|
||||||
val sheetBackCallback =
|
sheetBackCallback =
|
||||||
SheetBackPressedCallback(
|
SheetBackPressedCallback(
|
||||||
playbackSheetBehavior = playbackSheetBehavior,
|
playbackSheetBehavior = playbackSheetBehavior,
|
||||||
queueSheetBehavior = queueSheetBehavior)
|
queueSheetBehavior = queueSheetBehavior)
|
||||||
.also { sheetBackCallback = it }
|
|
||||||
val detailBackCallback =
|
val detailBackCallback =
|
||||||
DetailBackPressedCallback(detailModel).also { detailBackCallback = it }
|
DetailBackPressedCallback(detailModel).also { detailBackCallback = it }
|
||||||
val selectionBackCallback =
|
val selectionBackCallback =
|
||||||
SelectionBackPressedCallback(listModel).also { selectionBackCallback = it }
|
SelectionBackPressedCallback(listModel).also { selectionBackCallback = it }
|
||||||
val exploreBackCallback =
|
|
||||||
ExploreBackPressedCallback(binding.exploreNavHost).also { exploreBackCallback = it }
|
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
val context = requireActivity()
|
val context = requireActivity()
|
||||||
// Override the back pressed listener so we can map back navigation to collapsing
|
|
||||||
// navigation, navigation out of detail views, etc.
|
|
||||||
context.onBackPressedDispatcher.apply {
|
|
||||||
addCallback(viewLifecycleOwner, exploreBackCallback)
|
|
||||||
addCallback(viewLifecycleOwner, selectionBackCallback)
|
|
||||||
addCallback(viewLifecycleOwner, detailBackCallback)
|
|
||||||
addCallback(viewLifecycleOwner, sheetBackCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.root.setOnApplyWindowInsetsListener { _, insets ->
|
binding.root.setOnApplyWindowInsetsListener { _, insets ->
|
||||||
lastInsets = insets
|
lastInsets = insets
|
||||||
|
|
@ -152,6 +151,7 @@ class MainFragment :
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
collectImmediately(detailModel.editedPlaylist, detailBackCallback::invalidateEnabled)
|
collectImmediately(detailModel.editedPlaylist, detailBackCallback::invalidateEnabled)
|
||||||
|
collectImmediately(homeModel.showOuter.flow, ::handleShowOuter)
|
||||||
collectImmediately(listModel.selected, selectionBackCallback::invalidateEnabled)
|
collectImmediately(listModel.selected, selectionBackCallback::invalidateEnabled)
|
||||||
collectImmediately(playbackModel.song, ::updateSong)
|
collectImmediately(playbackModel.song, ::updateSong)
|
||||||
collectImmediately(playbackModel.openPanel.flow, ::handlePanel)
|
collectImmediately(playbackModel.openPanel.flow, ::handlePanel)
|
||||||
|
|
@ -169,6 +169,18 @@ class MainFragment :
|
||||||
binding.playbackSheet.viewTreeObserver.addOnPreDrawListener(this@MainFragment)
|
binding.playbackSheet.viewTreeObserver.addOnPreDrawListener(this@MainFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
// Override the back pressed listener so we can map back navigation to collapsing
|
||||||
|
// navigation, navigation out of detail views, etc. We have to do this here in
|
||||||
|
// onResume or otherwise the FragmentManager will have precedence.
|
||||||
|
requireActivity().onBackPressedDispatcher.apply {
|
||||||
|
addCallback(viewLifecycleOwner, requireNotNull(selectionBackCallback))
|
||||||
|
addCallback(viewLifecycleOwner, requireNotNull(detailBackCallback))
|
||||||
|
addCallback(viewLifecycleOwner, requireNotNull(sheetBackCallback))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
|
|
@ -181,7 +193,6 @@ class MainFragment :
|
||||||
sheetBackCallback = null
|
sheetBackCallback = null
|
||||||
detailBackCallback = null
|
detailBackCallback = null
|
||||||
selectionBackCallback = null
|
selectionBackCallback = null
|
||||||
exploreBackCallback = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPreDraw(): Boolean {
|
override fun onPreDraw(): Boolean {
|
||||||
|
|
@ -283,8 +294,6 @@ class MainFragment :
|
||||||
// Drop the initial call by NavController that simply provides us with the current
|
// Drop the initial call by NavController that simply provides us with the current
|
||||||
// destination. This would cause the selection state to be lost every time the device
|
// destination. This would cause the selection state to be lost every time the device
|
||||||
// rotates.
|
// rotates.
|
||||||
requireNotNull(exploreBackCallback) { "ExploreBackPressedCallback was not available" }
|
|
||||||
.invalidateEnabled()
|
|
||||||
if (!initialNavDestinationChange) {
|
if (!initialNavDestinationChange) {
|
||||||
initialNavDestinationChange = true
|
initialNavDestinationChange = true
|
||||||
return
|
return
|
||||||
|
|
@ -292,6 +301,17 @@ class MainFragment :
|
||||||
listModel.dropSelection()
|
listModel.dropSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleShowOuter(outer: Outer?) {
|
||||||
|
val directions =
|
||||||
|
when (outer) {
|
||||||
|
is Outer.Settings -> MainFragmentDirections.preferences()
|
||||||
|
is Outer.About -> MainFragmentDirections.about()
|
||||||
|
null -> return
|
||||||
|
}
|
||||||
|
findNavController().navigateSafe(directions)
|
||||||
|
homeModel.showOuter.consume()
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateSong(song: Song?) {
|
private fun updateSong(song: Song?) {
|
||||||
if (song != null) {
|
if (song != null) {
|
||||||
tryShowSheets()
|
tryShowSheets()
|
||||||
|
|
@ -462,23 +482,4 @@ class MainFragment :
|
||||||
isEnabled = selection.isNotEmpty()
|
isEnabled = selection.isNotEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class ExploreBackPressedCallback(
|
|
||||||
private val exploreNavHost: FragmentContainerView
|
|
||||||
) : OnBackPressedCallback(false) {
|
|
||||||
// Note: We cannot cache the NavController in a variable since it's current destination
|
|
||||||
// value goes stale for some reason.
|
|
||||||
|
|
||||||
override fun handleOnBackPressed() {
|
|
||||||
exploreNavHost.findNavController().navigateUp()
|
|
||||||
logD("Forwarded back navigation to explore nav host")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invalidateEnabled() {
|
|
||||||
val exploreNavController = exploreNavHost.findNavController()
|
|
||||||
isEnabled =
|
|
||||||
exploreNavController.currentDestination?.id !=
|
|
||||||
exploreNavController.graph.startDestinationId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,6 @@ package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
|
|
@ -40,11 +38,9 @@ import org.oxycblt.auxio.list.Header
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
import org.oxycblt.auxio.list.ListViewModel
|
import org.oxycblt.auxio.list.ListViewModel
|
||||||
import org.oxycblt.auxio.list.Menu
|
import org.oxycblt.auxio.list.menu.Menu
|
||||||
import org.oxycblt.auxio.list.Sort
|
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.PlaylistDecision
|
import org.oxycblt.auxio.music.PlaylistDecision
|
||||||
|
|
@ -56,11 +52,9 @@ import org.oxycblt.auxio.util.canScroll
|
||||||
import org.oxycblt.auxio.util.collect
|
import org.oxycblt.auxio.util.collect
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
|
||||||
import org.oxycblt.auxio.util.navigateSafe
|
import org.oxycblt.auxio.util.navigateSafe
|
||||||
|
import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
|
||||||
import org.oxycblt.auxio.util.setFullWidthLookup
|
import org.oxycblt.auxio.util.setFullWidthLookup
|
||||||
import org.oxycblt.auxio.util.share
|
|
||||||
import org.oxycblt.auxio.util.showToast
|
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -77,6 +71,7 @@ class AlbumDetailFragment :
|
||||||
override val listModel: ListViewModel by activityViewModels()
|
override val listModel: ListViewModel by activityViewModels()
|
||||||
override val musicModel: MusicViewModel by activityViewModels()
|
override val musicModel: MusicViewModel by activityViewModels()
|
||||||
override val playbackModel: PlaybackViewModel by activityViewModels()
|
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
|
||||||
// Information about what album to display is initially within the navigation arguments
|
// Information about what album to display is initially within the navigation arguments
|
||||||
// as a UID, as that is the only safe way to parcel an album.
|
// as a UID, as that is the only safe way to parcel an album.
|
||||||
private val args: AlbumDetailFragmentArgs by navArgs()
|
private val args: AlbumDetailFragmentArgs by navArgs()
|
||||||
|
|
@ -103,16 +98,18 @@ class AlbumDetailFragment :
|
||||||
|
|
||||||
// --- UI SETUP --
|
// --- UI SETUP --
|
||||||
binding.detailNormalToolbar.apply {
|
binding.detailNormalToolbar.apply {
|
||||||
inflateMenu(R.menu.toolbar_album)
|
|
||||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
setOnMenuItemClickListener(this@AlbumDetailFragment)
|
overrideOnOverflowMenuClick {
|
||||||
|
listModel.openMenu(
|
||||||
|
R.menu.item_detail_album, unlikelyToBeNull(detailModel.currentAlbum.value))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailRecycler.apply {
|
binding.detailRecycler.apply {
|
||||||
adapter = ConcatAdapter(albumHeaderAdapter, albumListAdapter)
|
adapter = ConcatAdapter(albumHeaderAdapter, albumListAdapter)
|
||||||
(layoutManager as GridLayoutManager).setFullWidthLookup {
|
(layoutManager as GridLayoutManager).setFullWidthLookup {
|
||||||
if (it != 0) {
|
if (it != 0) {
|
||||||
val item = detailModel.albumList.value[it - 1]
|
val item = detailModel.albumSongList.value[it - 1]
|
||||||
item is Divider || item is Header || item is Disc
|
item is Divider || item is Header || item is Disc
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
|
|
@ -124,7 +121,7 @@ class AlbumDetailFragment :
|
||||||
// DetailViewModel handles most initialization from the navigation argument.
|
// DetailViewModel handles most initialization from the navigation argument.
|
||||||
detailModel.setAlbum(args.albumUid)
|
detailModel.setAlbum(args.albumUid)
|
||||||
collectImmediately(detailModel.currentAlbum, ::updateAlbum)
|
collectImmediately(detailModel.currentAlbum, ::updateAlbum)
|
||||||
collectImmediately(detailModel.albumList, ::updateList)
|
collectImmediately(detailModel.albumSongList, ::updateList)
|
||||||
collect(detailModel.toShow.flow, ::handleShow)
|
collect(detailModel.toShow.flow, ::handleShow)
|
||||||
collect(listModel.menu.flow, ::handleMenu)
|
collect(listModel.menu.flow, ::handleMenu)
|
||||||
collectImmediately(listModel.selected, ::updateSelection)
|
collectImmediately(listModel.selected, ::updateSelection)
|
||||||
|
|
@ -140,52 +137,15 @@ class AlbumDetailFragment :
|
||||||
binding.detailRecycler.adapter = null
|
binding.detailRecycler.adapter = null
|
||||||
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
||||||
// during list initialization and crash the app. Could happen if the user is fast enough.
|
// during list initialization and crash the app. Could happen if the user is fast enough.
|
||||||
detailModel.albumInstructions.consume()
|
detailModel.albumSongInstructions.consume()
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
|
||||||
if (super.onMenuItemClick(item)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentAlbum = unlikelyToBeNull(detailModel.currentAlbum.value)
|
|
||||||
return when (item.itemId) {
|
|
||||||
R.id.action_play_next -> {
|
|
||||||
playbackModel.playNext(currentAlbum)
|
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_queue_add -> {
|
|
||||||
playbackModel.addToQueue(currentAlbum)
|
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_artist_details -> {
|
|
||||||
onNavigateToParentArtist()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_playlist_add -> {
|
|
||||||
musicModel.addToPlaylist(currentAlbum)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_share -> {
|
|
||||||
requireContext().share(currentAlbum)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
logW("Unexpected menu item selected")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRealClick(item: Song) {
|
override fun onRealClick(item: Song) {
|
||||||
// There can only be one album, so a null mode and an ALBUMS mode will function the same.
|
playbackModel.play(item, detailModel.playInAlbumWith)
|
||||||
playbackModel.playFrom(item, detailModel.playbackMode ?: MusicMode.ALBUMS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Song, anchor: View) {
|
override fun onOpenMenu(item: Song) {
|
||||||
listModel.openMenu(R.menu.item_album_song, item)
|
listModel.openMenu(R.menu.item_album_song, item, detailModel.playInAlbumWith)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlay() {
|
override fun onPlay() {
|
||||||
|
|
@ -196,31 +156,8 @@ class AlbumDetailFragment :
|
||||||
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentAlbum.value))
|
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentAlbum.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenSortMenu(anchor: View) {
|
override fun onOpenSortMenu() {
|
||||||
openMenu(anchor, R.menu.sort_album) {
|
findNavController().navigateSafe(AlbumDetailFragmentDirections.sort())
|
||||||
// Select the corresponding sort mode option
|
|
||||||
val sort = detailModel.albumSongSort
|
|
||||||
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
|
|
||||||
// Select the corresponding sort direction option
|
|
||||||
val directionItemId =
|
|
||||||
when (sort.direction) {
|
|
||||||
Sort.Direction.ASCENDING -> R.id.option_sort_asc
|
|
||||||
Sort.Direction.DESCENDING -> R.id.option_sort_dec
|
|
||||||
}
|
|
||||||
unlikelyToBeNull(menu.findItem(directionItemId)).isChecked = true
|
|
||||||
setOnMenuItemClickListener { item ->
|
|
||||||
item.isChecked = !item.isChecked
|
|
||||||
detailModel.albumSongSort =
|
|
||||||
when (item.itemId) {
|
|
||||||
// Sort direction options
|
|
||||||
R.id.option_sort_asc -> sort.withDirection(Sort.Direction.ASCENDING)
|
|
||||||
R.id.option_sort_dec -> sort.withDirection(Sort.Direction.DESCENDING)
|
|
||||||
// Any other option is a sort mode
|
|
||||||
else -> sort.withMode(unlikelyToBeNull(Sort.Mode.fromItemId(item.itemId)))
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigateToParentArtist() {
|
override fun onNavigateToParentArtist() {
|
||||||
|
|
@ -238,7 +175,7 @@ class AlbumDetailFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateList(list: List<Item>) {
|
private fun updateList(list: List<Item>) {
|
||||||
albumListAdapter.update(list, detailModel.albumInstructions.consume())
|
albumListAdapter.update(list, detailModel.albumSongInstructions.consume())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleShow(show: Show?) {
|
private fun handleShow(show: Show?) {
|
||||||
|
|
@ -304,9 +241,8 @@ class AlbumDetailFragment :
|
||||||
if (menu == null) return
|
if (menu == null) return
|
||||||
val directions =
|
val directions =
|
||||||
when (menu) {
|
when (menu) {
|
||||||
is Menu.ForSong ->
|
is Menu.ForSong -> AlbumDetailFragmentDirections.openSongMenu(menu.parcel)
|
||||||
AlbumDetailFragmentDirections.openSongMenu(menu.menuRes, menu.music.uid)
|
is Menu.ForAlbum -> AlbumDetailFragmentDirections.openAlbumMenu(menu.parcel)
|
||||||
is Menu.ForAlbum,
|
|
||||||
is Menu.ForArtist,
|
is Menu.ForArtist,
|
||||||
is Menu.ForGenre,
|
is Menu.ForGenre,
|
||||||
is Menu.ForPlaylist -> error("Unexpected menu $menu")
|
is Menu.ForPlaylist -> error("Unexpected menu $menu")
|
||||||
|
|
@ -365,7 +301,7 @@ class AlbumDetailFragment :
|
||||||
|
|
||||||
private fun scrollToAlbumSong(song: Song) {
|
private fun scrollToAlbumSong(song: Song) {
|
||||||
// Calculate where the item for the currently played song is
|
// Calculate where the item for the currently played song is
|
||||||
val pos = detailModel.albumList.value.indexOf(song)
|
val pos = detailModel.albumSongList.value.indexOf(song)
|
||||||
|
|
||||||
if (pos != -1) {
|
if (pos != -1) {
|
||||||
// Only scroll if the song is within this album.
|
// Only scroll if the song is within this album.
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,6 @@ package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
|
|
@ -40,8 +38,7 @@ import org.oxycblt.auxio.list.Header
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
import org.oxycblt.auxio.list.ListViewModel
|
import org.oxycblt.auxio.list.ListViewModel
|
||||||
import org.oxycblt.auxio.list.Menu
|
import org.oxycblt.auxio.list.menu.Menu
|
||||||
import org.oxycblt.auxio.list.Sort
|
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
|
|
@ -54,11 +51,9 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.util.collect
|
import org.oxycblt.auxio.util.collect
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
|
||||||
import org.oxycblt.auxio.util.navigateSafe
|
import org.oxycblt.auxio.util.navigateSafe
|
||||||
|
import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
|
||||||
import org.oxycblt.auxio.util.setFullWidthLookup
|
import org.oxycblt.auxio.util.setFullWidthLookup
|
||||||
import org.oxycblt.auxio.util.share
|
|
||||||
import org.oxycblt.auxio.util.showToast
|
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -101,9 +96,12 @@ class ArtistDetailFragment :
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
binding.detailNormalToolbar.apply {
|
binding.detailNormalToolbar.apply {
|
||||||
inflateMenu(R.menu.toolbar_parent)
|
|
||||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
setOnMenuItemClickListener(this@ArtistDetailFragment)
|
setOnMenuItemClickListener(this@ArtistDetailFragment)
|
||||||
|
overrideOnOverflowMenuClick {
|
||||||
|
listModel.openMenu(
|
||||||
|
R.menu.item_detail_parent, unlikelyToBeNull(detailModel.currentArtist.value))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailRecycler.apply {
|
binding.detailRecycler.apply {
|
||||||
|
|
@ -111,7 +109,7 @@ class ArtistDetailFragment :
|
||||||
(layoutManager as GridLayoutManager).setFullWidthLookup {
|
(layoutManager as GridLayoutManager).setFullWidthLookup {
|
||||||
if (it != 0) {
|
if (it != 0) {
|
||||||
val item =
|
val item =
|
||||||
detailModel.artistList.value.getOrElse(it - 1) {
|
detailModel.artistSongList.value.getOrElse(it - 1) {
|
||||||
return@setFullWidthLookup false
|
return@setFullWidthLookup false
|
||||||
}
|
}
|
||||||
item is Divider || item is Header
|
item is Divider || item is Header
|
||||||
|
|
@ -125,7 +123,7 @@ class ArtistDetailFragment :
|
||||||
// DetailViewModel handles most initialization from the navigation argument.
|
// DetailViewModel handles most initialization from the navigation argument.
|
||||||
detailModel.setArtist(args.artistUid)
|
detailModel.setArtist(args.artistUid)
|
||||||
collectImmediately(detailModel.currentArtist, ::updateArtist)
|
collectImmediately(detailModel.currentArtist, ::updateArtist)
|
||||||
collectImmediately(detailModel.artistList, ::updateList)
|
collectImmediately(detailModel.artistSongList, ::updateList)
|
||||||
collect(detailModel.toShow.flow, ::handleShow)
|
collect(detailModel.toShow.flow, ::handleShow)
|
||||||
collect(listModel.menu.flow, ::handleMenu)
|
collect(listModel.menu.flow, ::handleMenu)
|
||||||
collectImmediately(listModel.selected, ::updateSelection)
|
collectImmediately(listModel.selected, ::updateSelection)
|
||||||
|
|
@ -141,62 +139,21 @@ class ArtistDetailFragment :
|
||||||
binding.detailRecycler.adapter = null
|
binding.detailRecycler.adapter = null
|
||||||
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
||||||
// during list initialization and crash the app. Could happen if the user is fast enough.
|
// during list initialization and crash the app. Could happen if the user is fast enough.
|
||||||
detailModel.artistInstructions.consume()
|
detailModel.artistSongInstructions.consume()
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
|
||||||
if (super.onMenuItemClick(item)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentArtist = unlikelyToBeNull(detailModel.currentArtist.value)
|
|
||||||
return when (item.itemId) {
|
|
||||||
R.id.action_play_next -> {
|
|
||||||
playbackModel.playNext(currentArtist)
|
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_queue_add -> {
|
|
||||||
playbackModel.addToQueue(currentArtist)
|
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_playlist_add -> {
|
|
||||||
musicModel.addToPlaylist(currentArtist)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_share -> {
|
|
||||||
requireContext().share(currentArtist)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
logW("Unexpected menu item selected")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRealClick(item: Music) {
|
override fun onRealClick(item: Music) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Album -> detailModel.showAlbum(item)
|
is Album -> detailModel.showAlbum(item)
|
||||||
is Song -> {
|
is Song -> playbackModel.play(item, detailModel.playInArtistWith)
|
||||||
val playbackMode = detailModel.playbackMode
|
|
||||||
if (playbackMode != null) {
|
|
||||||
playbackModel.playFrom(item, playbackMode)
|
|
||||||
} else {
|
|
||||||
// When configured to play from the selected item, we already have an Artist
|
|
||||||
// to play from.
|
|
||||||
playbackModel.playFromArtist(
|
|
||||||
item, unlikelyToBeNull(detailModel.currentArtist.value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Music, anchor: View) {
|
override fun onOpenMenu(item: Music) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> listModel.openMenu(R.menu.item_artist_song, item)
|
is Song ->
|
||||||
|
listModel.openMenu(R.menu.item_artist_song, item, detailModel.playInArtistWith)
|
||||||
is Album -> listModel.openMenu(R.menu.item_artist_album, item)
|
is Album -> listModel.openMenu(R.menu.item_artist_album, item)
|
||||||
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
||||||
}
|
}
|
||||||
|
|
@ -210,33 +167,8 @@ class ArtistDetailFragment :
|
||||||
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentArtist.value))
|
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentArtist.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenSortMenu(anchor: View) {
|
override fun onOpenSortMenu() {
|
||||||
openMenu(anchor, R.menu.sort_artist) {
|
findNavController().navigateSafe(ArtistDetailFragmentDirections.sort())
|
||||||
// Select the corresponding sort mode option
|
|
||||||
val sort = detailModel.artistSongSort
|
|
||||||
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
|
|
||||||
// Select the corresponding sort direction option
|
|
||||||
val directionItemId =
|
|
||||||
when (sort.direction) {
|
|
||||||
Sort.Direction.ASCENDING -> R.id.option_sort_asc
|
|
||||||
Sort.Direction.DESCENDING -> R.id.option_sort_dec
|
|
||||||
}
|
|
||||||
unlikelyToBeNull(menu.findItem(directionItemId)).isChecked = true
|
|
||||||
setOnMenuItemClickListener { item ->
|
|
||||||
item.isChecked = !item.isChecked
|
|
||||||
|
|
||||||
detailModel.artistSongSort =
|
|
||||||
when (item.itemId) {
|
|
||||||
// Sort direction options
|
|
||||||
R.id.option_sort_asc -> sort.withDirection(Sort.Direction.ASCENDING)
|
|
||||||
R.id.option_sort_dec -> sort.withDirection(Sort.Direction.DESCENDING)
|
|
||||||
// Any other option is a sort mode
|
|
||||||
else -> sort.withMode(unlikelyToBeNull(Sort.Mode.fromItemId(item.itemId)))
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateArtist(artist: Artist?) {
|
private fun updateArtist(artist: Artist?) {
|
||||||
|
|
@ -245,24 +177,12 @@ class ArtistDetailFragment :
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
requireBinding().detailNormalToolbar.apply {
|
requireBinding().detailNormalToolbar.title = artist.name.resolve(requireContext())
|
||||||
title = artist.name.resolve(requireContext())
|
|
||||||
|
|
||||||
// Disable options that make no sense with an empty artist
|
|
||||||
val playable = artist.songs.isNotEmpty()
|
|
||||||
if (!playable) {
|
|
||||||
logD("Artist is empty, disabling playback/playlist/share options")
|
|
||||||
}
|
|
||||||
menu.findItem(R.id.action_play_next).isEnabled = playable
|
|
||||||
menu.findItem(R.id.action_queue_add).isEnabled = playable
|
|
||||||
menu.findItem(R.id.action_playlist_add).isEnabled = playable
|
|
||||||
menu.findItem(R.id.action_share).isEnabled = playable
|
|
||||||
}
|
|
||||||
artistHeaderAdapter.setParent(artist)
|
artistHeaderAdapter.setParent(artist)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateList(list: List<Item>) {
|
private fun updateList(list: List<Item>) {
|
||||||
artistListAdapter.update(list, detailModel.artistInstructions.consume())
|
artistListAdapter.update(list, detailModel.artistSongInstructions.consume())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleShow(show: Show?) {
|
private fun handleShow(show: Show?) {
|
||||||
|
|
@ -316,11 +236,9 @@ class ArtistDetailFragment :
|
||||||
if (menu == null) return
|
if (menu == null) return
|
||||||
val directions =
|
val directions =
|
||||||
when (menu) {
|
when (menu) {
|
||||||
is Menu.ForSong ->
|
is Menu.ForSong -> ArtistDetailFragmentDirections.openSongMenu(menu.parcel)
|
||||||
ArtistDetailFragmentDirections.openSongMenu(menu.menuRes, menu.music.uid)
|
is Menu.ForAlbum -> ArtistDetailFragmentDirections.openAlbumMenu(menu.parcel)
|
||||||
is Menu.ForAlbum ->
|
is Menu.ForArtist -> ArtistDetailFragmentDirections.openArtistMenu(menu.parcel)
|
||||||
ArtistDetailFragmentDirections.openAlbumMenu(menu.menuRes, menu.music.uid)
|
|
||||||
is Menu.ForArtist,
|
|
||||||
is Menu.ForGenre,
|
is Menu.ForGenre,
|
||||||
is Menu.ForPlaylist -> error("Unexpected menu $menu")
|
is Menu.ForPlaylist -> error("Unexpected menu $menu")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,10 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow()
|
super.onAttachedToWindow()
|
||||||
|
if (!isInEditMode) {
|
||||||
(layoutParams as CoordinatorLayout.LayoutParams).behavior = Behavior(context)
|
(layoutParams as CoordinatorLayout.LayoutParams).behavior = Behavior(context)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun findTitleView(): TextView {
|
private fun findTitleView(): TextView {
|
||||||
val titleView = titleView
|
val titleView = titleView
|
||||||
|
|
|
||||||
|
|
@ -36,24 +36,25 @@ import org.oxycblt.auxio.detail.list.SortHeader
|
||||||
import org.oxycblt.auxio.list.BasicHeader
|
import org.oxycblt.auxio.list.BasicHeader
|
||||||
import org.oxycblt.auxio.list.Divider
|
import org.oxycblt.auxio.list.Divider
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.ListSettings
|
||||||
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||||
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.info.ReleaseType
|
import org.oxycblt.auxio.music.info.ReleaseType
|
||||||
import org.oxycblt.auxio.music.metadata.AudioProperties
|
import org.oxycblt.auxio.music.metadata.AudioProperties
|
||||||
|
import org.oxycblt.auxio.playback.PlaySong
|
||||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
import org.oxycblt.auxio.util.Event
|
import org.oxycblt.auxio.util.Event
|
||||||
import org.oxycblt.auxio.util.MutableEvent
|
import org.oxycblt.auxio.util.MutableEvent
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [ViewModel] that manages the Song, Album, Artist, and Genre detail views. Keeps track of the
|
* [ViewModel] that manages the Song, Album, Artist, and Genre detail views. Keeps track of the
|
||||||
|
|
@ -65,9 +66,9 @@ import org.oxycblt.auxio.util.logW
|
||||||
class DetailViewModel
|
class DetailViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
|
private val listSettings: ListSettings,
|
||||||
private val musicRepository: MusicRepository,
|
private val musicRepository: MusicRepository,
|
||||||
private val audioPropertiesFactory: AudioProperties.Factory,
|
private val audioPropertiesFactory: AudioProperties.Factory,
|
||||||
private val musicSettings: MusicSettings,
|
|
||||||
private val playbackSettings: PlaybackSettings
|
private val playbackSettings: PlaybackSettings
|
||||||
) : ViewModel(), MusicRepository.UpdateListener {
|
) : ViewModel(), MusicRepository.UpdateListener {
|
||||||
private val _toShow = MutableEvent<Show>()
|
private val _toShow = MutableEvent<Show>()
|
||||||
|
|
@ -97,23 +98,23 @@ constructor(
|
||||||
val currentAlbum: StateFlow<Album?>
|
val currentAlbum: StateFlow<Album?>
|
||||||
get() = _currentAlbum
|
get() = _currentAlbum
|
||||||
|
|
||||||
private val _albumList = MutableStateFlow(listOf<Item>())
|
private val _albumSongList = MutableStateFlow(listOf<Item>())
|
||||||
/** The current list data derived from [currentAlbum]. */
|
/** The current list data derived from [currentAlbum]. */
|
||||||
val albumList: StateFlow<List<Item>>
|
val albumSongList: StateFlow<List<Item>>
|
||||||
get() = _albumList
|
get() = _albumSongList
|
||||||
private val _albumInstructions = MutableEvent<UpdateInstructions>()
|
|
||||||
/** Instructions for updating [albumList] in the UI. */
|
|
||||||
val albumInstructions: Event<UpdateInstructions>
|
|
||||||
get() = _albumInstructions
|
|
||||||
|
|
||||||
/** The current [Sort] used for [Song]s in [albumList]. */
|
private val _albumSongInstructions = MutableEvent<UpdateInstructions>()
|
||||||
var albumSongSort: Sort
|
/** Instructions for updating [albumSongList] in the UI. */
|
||||||
get() = musicSettings.albumSongSort
|
val albumSongInstructions: Event<UpdateInstructions>
|
||||||
set(value) {
|
get() = _albumSongInstructions
|
||||||
musicSettings.albumSongSort = value
|
|
||||||
// Refresh the album list to reflect the new sort.
|
/** The current [Sort] used for [Song]s in [albumSongList]. */
|
||||||
currentAlbum.value?.let { refreshAlbumList(it, true) }
|
val albumSongSort: Sort
|
||||||
}
|
get() = listSettings.albumSongSort
|
||||||
|
|
||||||
|
/** The [PlaySong] instructions to use when playing a [Song] from [Album] details. */
|
||||||
|
val playInAlbumWith
|
||||||
|
get() = playbackSettings.inParentPlaybackMode ?: PlaySong.FromAlbum
|
||||||
|
|
||||||
// --- ARTIST ---
|
// --- ARTIST ---
|
||||||
|
|
||||||
|
|
@ -122,23 +123,28 @@ constructor(
|
||||||
val currentArtist: StateFlow<Artist?>
|
val currentArtist: StateFlow<Artist?>
|
||||||
get() = _currentArtist
|
get() = _currentArtist
|
||||||
|
|
||||||
private val _artistList = MutableStateFlow(listOf<Item>())
|
private val _artistSongList = MutableStateFlow(listOf<Item>())
|
||||||
/** The current list derived from [currentArtist]. */
|
/** The current list derived from [currentArtist]. */
|
||||||
val artistList: StateFlow<List<Item>> = _artistList
|
val artistSongList: StateFlow<List<Item>> = _artistSongList
|
||||||
private val _artistInstructions = MutableEvent<UpdateInstructions>()
|
|
||||||
/** Instructions for updating [artistList] in the UI. */
|
|
||||||
val artistInstructions: Event<UpdateInstructions>
|
|
||||||
get() = _artistInstructions
|
|
||||||
|
|
||||||
/** The current [Sort] used for [Song]s in [artistList]. */
|
private val _artistSongInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
/** Instructions for updating [artistSongList] in the UI. */
|
||||||
|
val artistSongInstructions: Event<UpdateInstructions>
|
||||||
|
get() = _artistSongInstructions
|
||||||
|
|
||||||
|
/** The current [Sort] used for [Song]s in [artistSongList]. */
|
||||||
var artistSongSort: Sort
|
var artistSongSort: Sort
|
||||||
get() = musicSettings.artistSongSort
|
get() = listSettings.artistSongSort
|
||||||
set(value) {
|
set(value) {
|
||||||
musicSettings.artistSongSort = value
|
listSettings.artistSongSort = value
|
||||||
// Refresh the artist list to reflect the new sort.
|
// Refresh the artist list to reflect the new sort.
|
||||||
currentArtist.value?.let { refreshArtistList(it, true) }
|
currentArtist.value?.let { refreshArtistList(it, true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The [PlaySong] instructions to use when playing a [Song] from [Artist] details. */
|
||||||
|
val playInArtistWith
|
||||||
|
get() = playbackSettings.inParentPlaybackMode ?: PlaySong.FromArtist(currentArtist.value)
|
||||||
|
|
||||||
// --- GENRE ---
|
// --- GENRE ---
|
||||||
|
|
||||||
private val _currentGenre = MutableStateFlow<Genre?>(null)
|
private val _currentGenre = MutableStateFlow<Genre?>(null)
|
||||||
|
|
@ -146,23 +152,28 @@ constructor(
|
||||||
val currentGenre: StateFlow<Genre?>
|
val currentGenre: StateFlow<Genre?>
|
||||||
get() = _currentGenre
|
get() = _currentGenre
|
||||||
|
|
||||||
private val _genreList = MutableStateFlow(listOf<Item>())
|
private val _genreSongList = MutableStateFlow(listOf<Item>())
|
||||||
/** The current list data derived from [currentGenre]. */
|
/** The current list data derived from [currentGenre]. */
|
||||||
val genreList: StateFlow<List<Item>> = _genreList
|
val genreSongList: StateFlow<List<Item>> = _genreSongList
|
||||||
private val _genreInstructions = MutableEvent<UpdateInstructions>()
|
|
||||||
/** Instructions for updating [artistList] in the UI. */
|
|
||||||
val genreInstructions: Event<UpdateInstructions>
|
|
||||||
get() = _genreInstructions
|
|
||||||
|
|
||||||
/** The current [Sort] used for [Song]s in [genreList]. */
|
private val _genreSongInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
/** Instructions for updating [artistSongList] in the UI. */
|
||||||
|
val genreSongInstructions: Event<UpdateInstructions>
|
||||||
|
get() = _genreSongInstructions
|
||||||
|
|
||||||
|
/** The current [Sort] used for [Song]s in [genreSongList]. */
|
||||||
var genreSongSort: Sort
|
var genreSongSort: Sort
|
||||||
get() = musicSettings.genreSongSort
|
get() = listSettings.genreSongSort
|
||||||
set(value) {
|
set(value) {
|
||||||
musicSettings.genreSongSort = value
|
listSettings.genreSongSort = value
|
||||||
// Refresh the genre list to reflect the new sort.
|
// Refresh the genre list to reflect the new sort.
|
||||||
currentGenre.value?.let { refreshGenreList(it, true) }
|
currentGenre.value?.let { refreshGenreList(it, true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The [PlaySong] instructions to use when playing a [Song] from [Genre] details. */
|
||||||
|
val playInGenreWith
|
||||||
|
get() = playbackSettings.inParentPlaybackMode ?: PlaySong.FromGenre(currentGenre.value)
|
||||||
|
|
||||||
// --- PLAYLIST ---
|
// --- PLAYLIST ---
|
||||||
|
|
||||||
private val _currentPlaylist = MutableStateFlow<Playlist?>(null)
|
private val _currentPlaylist = MutableStateFlow<Playlist?>(null)
|
||||||
|
|
@ -170,13 +181,14 @@ constructor(
|
||||||
val currentPlaylist: StateFlow<Playlist?>
|
val currentPlaylist: StateFlow<Playlist?>
|
||||||
get() = _currentPlaylist
|
get() = _currentPlaylist
|
||||||
|
|
||||||
private val _playlistList = MutableStateFlow(listOf<Item>())
|
private val _playlistSongList = MutableStateFlow(listOf<Item>())
|
||||||
/** The current list data derived from [currentPlaylist] */
|
/** The current list data derived from [currentPlaylist] */
|
||||||
val playlistList: StateFlow<List<Item>> = _playlistList
|
val playlistSongList: StateFlow<List<Item>> = _playlistSongList
|
||||||
private val _playlistInstructions = MutableEvent<UpdateInstructions>()
|
|
||||||
/** Instructions for updating [playlistList] in the UI. */
|
private val _playlistSongInstructions = MutableEvent<UpdateInstructions>()
|
||||||
val playlistInstructions: Event<UpdateInstructions>
|
/** Instructions for updating [playlistSongList] in the UI. */
|
||||||
get() = _playlistInstructions
|
val playlistSongInstructions: Event<UpdateInstructions>
|
||||||
|
get() = _playlistSongInstructions
|
||||||
|
|
||||||
private val _editedPlaylist = MutableStateFlow<List<Song>?>(null)
|
private val _editedPlaylist = MutableStateFlow<List<Song>?>(null)
|
||||||
/**
|
/**
|
||||||
|
|
@ -186,12 +198,11 @@ constructor(
|
||||||
val editedPlaylist: StateFlow<List<Song>?>
|
val editedPlaylist: StateFlow<List<Song>?>
|
||||||
get() = _editedPlaylist
|
get() = _editedPlaylist
|
||||||
|
|
||||||
/**
|
/** The [PlaySong] instructions to use when playing a [Song] from [Genre] details. */
|
||||||
* The [MusicMode] to use when playing a [Song] from the UI, or null to play from the currently
|
val playInPlaylistWith
|
||||||
* shown item.
|
get() =
|
||||||
*/
|
playbackSettings.inParentPlaybackMode
|
||||||
val playbackMode: MusicMode?
|
?: PlaySong.FromPlaylist(unlikelyToBeNull(currentPlaylist.value))
|
||||||
get() = playbackSettings.inParentPlaybackMode
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
musicRepository.addUpdateListener(this)
|
musicRepository.addUpdateListener(this)
|
||||||
|
|
@ -338,7 +349,7 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a new [currentAlbum] from it's [Music.UID]. [currentAlbum] and [albumList] will be
|
* Set a new [currentAlbum] from it's [Music.UID]. [currentAlbum] and [albumSongList] will be
|
||||||
* updated to align with the new [Album].
|
* updated to align with the new [Album].
|
||||||
*
|
*
|
||||||
* @param uid The [Music.UID] of the [Album] to update [currentAlbum] to. Must be valid.
|
* @param uid The [Music.UID] of the [Album] to update [currentAlbum] to. Must be valid.
|
||||||
|
|
@ -353,7 +364,17 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a new [currentArtist] from it's [Music.UID]. [currentArtist] and [artistList] will be
|
* Apply a new [Sort] to [albumSongList].
|
||||||
|
*
|
||||||
|
* @param sort The [Sort] to apply.
|
||||||
|
*/
|
||||||
|
fun applyAlbumSongSort(sort: Sort) {
|
||||||
|
listSettings.albumSongSort = sort
|
||||||
|
_currentAlbum.value?.let { refreshAlbumList(it, true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new [currentArtist] from it's [Music.UID]. [currentArtist] and [artistSongList] will be
|
||||||
* updated to align with the new [Artist].
|
* updated to align with the new [Artist].
|
||||||
*
|
*
|
||||||
* @param uid The [Music.UID] of the [Artist] to update [currentArtist] to. Must be valid.
|
* @param uid The [Music.UID] of the [Artist] to update [currentArtist] to. Must be valid.
|
||||||
|
|
@ -368,7 +389,17 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a new [currentGenre] from it's [Music.UID]. [currentGenre] and [genreList] will be
|
* Apply a new [Sort] to [artistSongList].
|
||||||
|
*
|
||||||
|
* @param sort The [Sort] to apply.
|
||||||
|
*/
|
||||||
|
fun applyArtistSongSort(sort: Sort) {
|
||||||
|
listSettings.artistSongSort = sort
|
||||||
|
_currentArtist.value?.let { refreshArtistList(it, true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new [currentGenre] from it's [Music.UID]. [currentGenre] and [genreSongList] will be
|
||||||
* updated to align with the new album.
|
* updated to align with the new album.
|
||||||
*
|
*
|
||||||
* @param uid The [Music.UID] of the [Genre] to update [currentGenre] to. Must be valid.
|
* @param uid The [Music.UID] of the [Genre] to update [currentGenre] to. Must be valid.
|
||||||
|
|
@ -382,6 +413,16 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a new [Sort] to [genreSongList].
|
||||||
|
*
|
||||||
|
* @param sort The [Sort] to apply.
|
||||||
|
*/
|
||||||
|
fun applyGenreSongSort(sort: Sort) {
|
||||||
|
listSettings.genreSongSort = sort
|
||||||
|
_currentGenre.value?.let { refreshGenreList(it, true) }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a new [currentPlaylist] from it's [Music.UID]. If the [Music.UID] differs,
|
* Set a new [currentPlaylist] from it's [Music.UID]. If the [Music.UID] differs,
|
||||||
* [currentPlaylist] and [currentPlaylist] will be updated to align with the new album.
|
* [currentPlaylist] and [currentPlaylist] will be updated to align with the new album.
|
||||||
|
|
@ -439,6 +480,17 @@ constructor(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a [Sort] to the edited playlist. Does nothing if not in an editing session.
|
||||||
|
*
|
||||||
|
* @param sort The [Sort] to apply.
|
||||||
|
*/
|
||||||
|
fun applyPlaylistSongSort(sort: Sort) {
|
||||||
|
val playlist = _currentPlaylist.value ?: return
|
||||||
|
_editedPlaylist.value = sort.songs(_editedPlaylist.value ?: return)
|
||||||
|
refreshPlaylistList(playlist, UpdateInstructions.Replace(2))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (Visually) move a song in the current playlist. Does nothing if not in an editing session.
|
* (Visually) move a song in the current playlist. Does nothing if not in an editing session.
|
||||||
*
|
*
|
||||||
|
|
@ -447,7 +499,6 @@ constructor(
|
||||||
* @return true if the song was moved, false otherwise.
|
* @return true if the song was moved, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun movePlaylistSongs(from: Int, to: Int): Boolean {
|
fun movePlaylistSongs(from: Int, to: Int): Boolean {
|
||||||
// TODO: Song re-sorting
|
|
||||||
val playlist = _currentPlaylist.value ?: return false
|
val playlist = _currentPlaylist.value ?: return false
|
||||||
val editedPlaylist = (_editedPlaylist.value ?: return false).toMutableList()
|
val editedPlaylist = (_editedPlaylist.value ?: return false).toMutableList()
|
||||||
val realFrom = from - 2
|
val realFrom = from - 2
|
||||||
|
|
@ -531,8 +582,8 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Update album list to ${list.size} items with $instructions")
|
logD("Update album list to ${list.size} items with $instructions")
|
||||||
_albumInstructions.put(instructions)
|
_albumSongInstructions.put(instructions)
|
||||||
_albumList.value = list
|
_albumSongList.value = list
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshArtistList(artist: Artist, replace: Boolean = false) {
|
private fun refreshArtistList(artist: Artist, replace: Boolean = false) {
|
||||||
|
|
@ -594,8 +645,8 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Updating artist list to ${list.size} items with $instructions")
|
logD("Updating artist list to ${list.size} items with $instructions")
|
||||||
_artistInstructions.put(instructions)
|
_artistSongInstructions.put(instructions)
|
||||||
_artistList.value = list.toList()
|
_artistSongList.value = list.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshGenreList(genre: Genre, replace: Boolean = false) {
|
private fun refreshGenreList(genre: Genre, replace: Boolean = false) {
|
||||||
|
|
@ -620,8 +671,8 @@ constructor(
|
||||||
list.addAll(genreSongSort.songs(genre.songs))
|
list.addAll(genreSongSort.songs(genre.songs))
|
||||||
|
|
||||||
logD("Updating genre list to ${list.size} items with $instructions")
|
logD("Updating genre list to ${list.size} items with $instructions")
|
||||||
_genreInstructions.put(instructions)
|
_genreSongInstructions.put(instructions)
|
||||||
_genreList.value = list
|
_genreSongList.value = list
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshPlaylistList(
|
private fun refreshPlaylistList(
|
||||||
|
|
@ -640,8 +691,8 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Updating playlist list to ${list.size} items with $instructions")
|
logD("Updating playlist list to ${list.size} items with $instructions")
|
||||||
_playlistInstructions.put(instructions)
|
_playlistSongInstructions.put(instructions)
|
||||||
_playlistList.value = list
|
_playlistSongList.value = list
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,6 @@ package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
|
|
@ -40,8 +38,7 @@ import org.oxycblt.auxio.list.Header
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
import org.oxycblt.auxio.list.ListViewModel
|
import org.oxycblt.auxio.list.ListViewModel
|
||||||
import org.oxycblt.auxio.list.Menu
|
import org.oxycblt.auxio.list.menu.Menu
|
||||||
import org.oxycblt.auxio.list.Sort
|
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
|
|
@ -54,11 +51,9 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.util.collect
|
import org.oxycblt.auxio.util.collect
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
|
||||||
import org.oxycblt.auxio.util.navigateSafe
|
import org.oxycblt.auxio.util.navigateSafe
|
||||||
|
import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
|
||||||
import org.oxycblt.auxio.util.setFullWidthLookup
|
import org.oxycblt.auxio.util.setFullWidthLookup
|
||||||
import org.oxycblt.auxio.util.share
|
|
||||||
import org.oxycblt.auxio.util.showToast
|
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -99,9 +94,12 @@ class GenreDetailFragment :
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
binding.detailNormalToolbar.apply {
|
binding.detailNormalToolbar.apply {
|
||||||
inflateMenu(R.menu.toolbar_parent)
|
|
||||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
setOnMenuItemClickListener(this@GenreDetailFragment)
|
setOnMenuItemClickListener(this@GenreDetailFragment)
|
||||||
|
overrideOnOverflowMenuClick {
|
||||||
|
listModel.openMenu(
|
||||||
|
R.menu.item_detail_parent, unlikelyToBeNull(detailModel.currentGenre.value))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailRecycler.apply {
|
binding.detailRecycler.apply {
|
||||||
|
|
@ -109,7 +107,7 @@ class GenreDetailFragment :
|
||||||
(layoutManager as GridLayoutManager).setFullWidthLookup {
|
(layoutManager as GridLayoutManager).setFullWidthLookup {
|
||||||
if (it != 0) {
|
if (it != 0) {
|
||||||
val item =
|
val item =
|
||||||
detailModel.genreList.value.getOrElse(it - 1) {
|
detailModel.genreSongList.value.getOrElse(it - 1) {
|
||||||
return@setFullWidthLookup false
|
return@setFullWidthLookup false
|
||||||
}
|
}
|
||||||
item is Divider || item is Header
|
item is Divider || item is Header
|
||||||
|
|
@ -123,7 +121,7 @@ class GenreDetailFragment :
|
||||||
// DetailViewModel handles most initialization from the navigation argument.
|
// DetailViewModel handles most initialization from the navigation argument.
|
||||||
detailModel.setGenre(args.genreUid)
|
detailModel.setGenre(args.genreUid)
|
||||||
collectImmediately(detailModel.currentGenre, ::updatePlaylist)
|
collectImmediately(detailModel.currentGenre, ::updatePlaylist)
|
||||||
collectImmediately(detailModel.genreList, ::updateList)
|
collectImmediately(detailModel.genreSongList, ::updateList)
|
||||||
collect(detailModel.toShow.flow, ::handleShow)
|
collect(detailModel.toShow.flow, ::handleShow)
|
||||||
collect(listModel.menu.flow, ::handleMenu)
|
collect(listModel.menu.flow, ::handleMenu)
|
||||||
collectImmediately(listModel.selected, ::updateSelection)
|
collectImmediately(listModel.selected, ::updateSelection)
|
||||||
|
|
@ -139,63 +137,21 @@ class GenreDetailFragment :
|
||||||
binding.detailRecycler.adapter = null
|
binding.detailRecycler.adapter = null
|
||||||
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
||||||
// during list initialization and crash the app. Could happen if the user is fast enough.
|
// during list initialization and crash the app. Could happen if the user is fast enough.
|
||||||
detailModel.genreInstructions.consume()
|
detailModel.genreSongInstructions.consume()
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
|
||||||
if (super.onMenuItemClick(item)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentGenre = unlikelyToBeNull(detailModel.currentGenre.value)
|
|
||||||
return when (item.itemId) {
|
|
||||||
R.id.action_play_next -> {
|
|
||||||
playbackModel.playNext(currentGenre)
|
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_queue_add -> {
|
|
||||||
playbackModel.addToQueue(currentGenre)
|
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_playlist_add -> {
|
|
||||||
musicModel.addToPlaylist(currentGenre)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_share -> {
|
|
||||||
requireContext().share(currentGenre)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
logW("Unexpected menu item selected")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRealClick(item: Music) {
|
override fun onRealClick(item: Music) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Artist -> detailModel.showArtist(item)
|
is Artist -> detailModel.showArtist(item)
|
||||||
is Song -> {
|
is Song -> playbackModel.play(item, detailModel.playInGenreWith)
|
||||||
val playbackMode = detailModel.playbackMode
|
|
||||||
if (playbackMode != null) {
|
|
||||||
playbackModel.playFrom(item, playbackMode)
|
|
||||||
} else {
|
|
||||||
// When configured to play from the selected item, we already have an Genre
|
|
||||||
// to play from.
|
|
||||||
playbackModel.playFromGenre(
|
|
||||||
item, unlikelyToBeNull(detailModel.currentGenre.value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Music, anchor: View) {
|
override fun onOpenMenu(item: Music) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Artist -> listModel.openMenu(R.menu.item_parent, item)
|
is Artist -> listModel.openMenu(R.menu.item_parent, item)
|
||||||
is Song -> listModel.openMenu(R.menu.item_song, item)
|
is Song -> listModel.openMenu(R.menu.item_song, item, detailModel.playInGenreWith)
|
||||||
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -208,31 +164,8 @@ class GenreDetailFragment :
|
||||||
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentGenre.value))
|
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentGenre.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenSortMenu(anchor: View) {
|
override fun onOpenSortMenu() {
|
||||||
openMenu(anchor, R.menu.sort_genre) {
|
findNavController().navigateSafe(GenreDetailFragmentDirections.sort())
|
||||||
// Select the corresponding sort mode option
|
|
||||||
val sort = detailModel.genreSongSort
|
|
||||||
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
|
|
||||||
// Select the corresponding sort direction option
|
|
||||||
val directionItemId =
|
|
||||||
when (sort.direction) {
|
|
||||||
Sort.Direction.ASCENDING -> R.id.option_sort_asc
|
|
||||||
Sort.Direction.DESCENDING -> R.id.option_sort_dec
|
|
||||||
}
|
|
||||||
unlikelyToBeNull(menu.findItem(directionItemId)).isChecked = true
|
|
||||||
setOnMenuItemClickListener { item ->
|
|
||||||
item.isChecked = !item.isChecked
|
|
||||||
detailModel.genreSongSort =
|
|
||||||
when (item.itemId) {
|
|
||||||
// Sort direction options
|
|
||||||
R.id.option_sort_asc -> sort.withDirection(Sort.Direction.ASCENDING)
|
|
||||||
R.id.option_sort_dec -> sort.withDirection(Sort.Direction.DESCENDING)
|
|
||||||
// Any other option is a sort mode
|
|
||||||
else -> sort.withMode(unlikelyToBeNull(Sort.Mode.fromItemId(item.itemId)))
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePlaylist(genre: Genre?) {
|
private fun updatePlaylist(genre: Genre?) {
|
||||||
|
|
@ -246,7 +179,7 @@ class GenreDetailFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateList(list: List<Item>) {
|
private fun updateList(list: List<Item>) {
|
||||||
genreListAdapter.update(list, detailModel.genreInstructions.consume())
|
genreListAdapter.update(list, detailModel.genreSongInstructions.consume())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleShow(show: Show?) {
|
private fun handleShow(show: Show?) {
|
||||||
|
|
@ -304,12 +237,10 @@ class GenreDetailFragment :
|
||||||
if (menu == null) return
|
if (menu == null) return
|
||||||
val directions =
|
val directions =
|
||||||
when (menu) {
|
when (menu) {
|
||||||
is Menu.ForSong ->
|
is Menu.ForSong -> GenreDetailFragmentDirections.openSongMenu(menu.parcel)
|
||||||
GenreDetailFragmentDirections.openSongMenu(menu.menuRes, menu.music.uid)
|
is Menu.ForArtist -> GenreDetailFragmentDirections.openArtistMenu(menu.parcel)
|
||||||
is Menu.ForArtist ->
|
is Menu.ForGenre -> GenreDetailFragmentDirections.openGenreMenu(menu.parcel)
|
||||||
GenreDetailFragmentDirections.openArtistMenu(menu.menuRes, menu.music.uid)
|
|
||||||
is Menu.ForAlbum,
|
is Menu.ForAlbum,
|
||||||
is Menu.ForGenre,
|
|
||||||
is Menu.ForPlaylist -> error("Unexpected menu $menu")
|
is Menu.ForPlaylist -> error("Unexpected menu $menu")
|
||||||
}
|
}
|
||||||
findNavController().navigateSafe(directions)
|
findNavController().navigateSafe(directions)
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,6 @@ package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavDestination
|
import androidx.navigation.NavDestination
|
||||||
|
|
@ -44,7 +42,7 @@ import org.oxycblt.auxio.list.Header
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
import org.oxycblt.auxio.list.ListViewModel
|
import org.oxycblt.auxio.list.ListViewModel
|
||||||
import org.oxycblt.auxio.list.Menu
|
import org.oxycblt.auxio.list.menu.Menu
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
|
|
@ -56,11 +54,9 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.util.collect
|
import org.oxycblt.auxio.util.collect
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
|
||||||
import org.oxycblt.auxio.util.navigateSafe
|
import org.oxycblt.auxio.util.navigateSafe
|
||||||
|
import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
|
||||||
import org.oxycblt.auxio.util.setFullWidthLookup
|
import org.oxycblt.auxio.util.setFullWidthLookup
|
||||||
import org.oxycblt.auxio.util.share
|
|
||||||
import org.oxycblt.auxio.util.showToast
|
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -104,9 +100,13 @@ class PlaylistDetailFragment :
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
binding.detailNormalToolbar.apply {
|
binding.detailNormalToolbar.apply {
|
||||||
inflateMenu(R.menu.toolbar_playlist)
|
|
||||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
setOnMenuItemClickListener(this@PlaylistDetailFragment)
|
setOnMenuItemClickListener(this@PlaylistDetailFragment)
|
||||||
|
overrideOnOverflowMenuClick {
|
||||||
|
listModel.openMenu(
|
||||||
|
R.menu.item_detail_playlist,
|
||||||
|
unlikelyToBeNull(detailModel.currentPlaylist.value))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailEditToolbar.apply {
|
binding.detailEditToolbar.apply {
|
||||||
|
|
@ -123,7 +123,7 @@ class PlaylistDetailFragment :
|
||||||
(layoutManager as GridLayoutManager).setFullWidthLookup {
|
(layoutManager as GridLayoutManager).setFullWidthLookup {
|
||||||
if (it != 0) {
|
if (it != 0) {
|
||||||
val item =
|
val item =
|
||||||
detailModel.playlistList.value.getOrElse(it - 1) {
|
detailModel.playlistSongList.value.getOrElse(it - 1) {
|
||||||
return@setFullWidthLookup false
|
return@setFullWidthLookup false
|
||||||
}
|
}
|
||||||
item is Divider || item is Header
|
item is Divider || item is Header
|
||||||
|
|
@ -137,7 +137,7 @@ class PlaylistDetailFragment :
|
||||||
// DetailViewModel handles most initialization from the navigation argument.
|
// DetailViewModel handles most initialization from the navigation argument.
|
||||||
detailModel.setPlaylist(args.playlistUid)
|
detailModel.setPlaylist(args.playlistUid)
|
||||||
collectImmediately(detailModel.currentPlaylist, ::updatePlaylist)
|
collectImmediately(detailModel.currentPlaylist, ::updatePlaylist)
|
||||||
collectImmediately(detailModel.playlistList, ::updateList)
|
collectImmediately(detailModel.playlistSongList, ::updateList)
|
||||||
collectImmediately(detailModel.editedPlaylist, ::updateEditedList)
|
collectImmediately(detailModel.editedPlaylist, ::updateEditedList)
|
||||||
collect(detailModel.toShow.flow, ::handleShow)
|
collect(detailModel.toShow.flow, ::handleShow)
|
||||||
collect(listModel.menu.flow, ::handleMenu)
|
collect(listModel.menu.flow, ::handleMenu)
|
||||||
|
|
@ -168,7 +168,7 @@ class PlaylistDetailFragment :
|
||||||
binding.detailRecycler.adapter = null
|
binding.detailRecycler.adapter = null
|
||||||
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
||||||
// during list initialization and crash the app. Could happen if the user is fast enough.
|
// during list initialization and crash the app. Could happen if the user is fast enough.
|
||||||
detailModel.playlistInstructions.consume()
|
detailModel.playlistSongInstructions.consume()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestinationChanged(
|
override fun onDestinationChanged(
|
||||||
|
|
@ -183,61 +183,24 @@ class PlaylistDetailFragment :
|
||||||
initialNavDestinationChange = true
|
initialNavDestinationChange = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (destination.id != R.id.playlist_detail_fragment &&
|
||||||
|
destination.id != R.id.playlist_song_sort_dialog) {
|
||||||
// Drop any pending playlist edits when navigating away. This could actually happen
|
// Drop any pending playlist edits when navigating away. This could actually happen
|
||||||
// if the user is quick enough.
|
// if the user is quick enough.
|
||||||
detailModel.dropPlaylistEdit()
|
detailModel.dropPlaylistEdit()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
|
||||||
if (super.onMenuItemClick(item)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentPlaylist = unlikelyToBeNull(detailModel.currentPlaylist.value)
|
|
||||||
return when (item.itemId) {
|
|
||||||
R.id.action_play_next -> {
|
|
||||||
playbackModel.playNext(currentPlaylist)
|
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_queue_add -> {
|
|
||||||
playbackModel.addToQueue(currentPlaylist)
|
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_rename -> {
|
|
||||||
musicModel.renamePlaylist(currentPlaylist)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_delete -> {
|
|
||||||
musicModel.deletePlaylist(currentPlaylist)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_share -> {
|
|
||||||
requireContext().share(currentPlaylist)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_save -> {
|
|
||||||
detailModel.savePlaylistEdit()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
logW("Unexpected menu item selected")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRealClick(item: Song) {
|
override fun onRealClick(item: Song) {
|
||||||
playbackModel.playFromPlaylist(item, unlikelyToBeNull(detailModel.currentPlaylist.value))
|
playbackModel.play(item, detailModel.playInPlaylistWith)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPickUp(viewHolder: RecyclerView.ViewHolder) {
|
override fun onPickUp(viewHolder: RecyclerView.ViewHolder) {
|
||||||
requireNotNull(touchHelper) { "ItemTouchHelper was not available" }.startDrag(viewHolder)
|
requireNotNull(touchHelper) { "ItemTouchHelper was not available" }.startDrag(viewHolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Song, anchor: View) {
|
override fun onOpenMenu(item: Song) {
|
||||||
listModel.openMenu(R.menu.item_playlist_song, item)
|
listModel.openMenu(R.menu.item_playlist_song, item, detailModel.playInPlaylistWith)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlay() {
|
override fun onPlay() {
|
||||||
|
|
@ -252,7 +215,9 @@ class PlaylistDetailFragment :
|
||||||
detailModel.startPlaylistEdit()
|
detailModel.startPlaylistEdit()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenSortMenu(anchor: View) {}
|
override fun onOpenSortMenu() {
|
||||||
|
findNavController().navigateSafe(PlaylistDetailFragmentDirections.sort())
|
||||||
|
}
|
||||||
|
|
||||||
private fun updatePlaylist(playlist: Playlist?) {
|
private fun updatePlaylist(playlist: Playlist?) {
|
||||||
if (playlist == null) {
|
if (playlist == null) {
|
||||||
|
|
@ -261,24 +226,14 @@ class PlaylistDetailFragment :
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
binding.detailNormalToolbar.apply {
|
binding.detailNormalToolbar.title = playlist.name.resolve(requireContext())
|
||||||
title = playlist.name.resolve(requireContext())
|
|
||||||
// Disable options that make no sense with an empty playlist
|
|
||||||
val playable = playlist.songs.isNotEmpty()
|
|
||||||
if (!playable) {
|
|
||||||
logD("Playlist is empty, disabling playback/share options")
|
|
||||||
}
|
|
||||||
menu.findItem(R.id.action_play_next).isEnabled = playable
|
|
||||||
menu.findItem(R.id.action_queue_add).isEnabled = playable
|
|
||||||
menu.findItem(R.id.action_share).isEnabled = playable
|
|
||||||
}
|
|
||||||
binding.detailEditToolbar.title =
|
binding.detailEditToolbar.title =
|
||||||
getString(R.string.fmt_editing, playlist.name.resolve(requireContext()))
|
getString(R.string.fmt_editing, playlist.name.resolve(requireContext()))
|
||||||
playlistHeaderAdapter.setParent(playlist)
|
playlistHeaderAdapter.setParent(playlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateList(list: List<Item>) {
|
private fun updateList(list: List<Item>) {
|
||||||
playlistListAdapter.update(list, detailModel.playlistInstructions.consume())
|
playlistListAdapter.update(list, detailModel.playlistSongInstructions.consume())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateEditedList(editedPlaylist: List<Song>?) {
|
private fun updateEditedList(editedPlaylist: List<Song>?) {
|
||||||
|
|
@ -344,12 +299,12 @@ class PlaylistDetailFragment :
|
||||||
if (menu == null) return
|
if (menu == null) return
|
||||||
val directions =
|
val directions =
|
||||||
when (menu) {
|
when (menu) {
|
||||||
is Menu.ForSong ->
|
is Menu.ForSong -> PlaylistDetailFragmentDirections.openSongMenu(menu.parcel)
|
||||||
PlaylistDetailFragmentDirections.openSongMenu(menu.menuRes, menu.music.uid)
|
is Menu.ForPlaylist ->
|
||||||
|
PlaylistDetailFragmentDirections.openPlaylistMenu(menu.parcel)
|
||||||
is Menu.ForArtist,
|
is Menu.ForArtist,
|
||||||
is Menu.ForAlbum,
|
is Menu.ForAlbum,
|
||||||
is Menu.ForGenre,
|
is Menu.ForGenre -> error("Unexpected menu $menu")
|
||||||
is Menu.ForPlaylist -> error("Unexpected menu $menu")
|
|
||||||
}
|
}
|
||||||
findNavController().navigateSafe(directions)
|
findNavController().navigateSafe(directions)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,16 +106,16 @@ sealed interface ArtistShowChoices {
|
||||||
class FromSong(val song: Song) : ArtistShowChoices {
|
class FromSong(val song: Song) : ArtistShowChoices {
|
||||||
override val uid = song.uid
|
override val uid = song.uid
|
||||||
override val choices = song.artists
|
override val choices = song.artists
|
||||||
|
|
||||||
override fun sanitize(newLibrary: DeviceLibrary) =
|
override fun sanitize(newLibrary: DeviceLibrary) =
|
||||||
newLibrary.findSong(uid)?.let { FromSong(it) }
|
newLibrary.findSong(uid)?.let { FromSong(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Backing implementation of [ArtistShowChoices] that is based on an [Album]. */
|
||||||
* Backing implementation of [ArtistShowChoices] that is based on an [AlbumArtistShowChoices].
|
|
||||||
*/
|
|
||||||
data class FromAlbum(val album: Album) : ArtistShowChoices {
|
data class FromAlbum(val album: Album) : ArtistShowChoices {
|
||||||
override val uid = album.uid
|
override val uid = album.uid
|
||||||
override val choices = album.artists
|
override val choices = album.artists
|
||||||
|
|
||||||
override fun sanitize(newLibrary: DeviceLibrary) =
|
override fun sanitize(newLibrary: DeviceLibrary) =
|
||||||
newLibrary.findAlbum(uid)?.let { FromAlbum(it) }
|
newLibrary.findAlbum(uid)?.let { FromAlbum(it) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ class ArtistDetailHeaderAdapter(private val listener: Listener) :
|
||||||
DetailHeaderAdapter<Artist, ArtistDetailHeaderViewHolder>() {
|
DetailHeaderAdapter<Artist, ArtistDetailHeaderViewHolder>() {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
ArtistDetailHeaderViewHolder.from(parent)
|
ArtistDetailHeaderViewHolder.from(parent)
|
||||||
|
|
||||||
override fun onBindHeader(holder: ArtistDetailHeaderViewHolder, parent: Artist) =
|
override fun onBindHeader(holder: ArtistDetailHeaderViewHolder, parent: Artist) =
|
||||||
holder.bind(parent, listener)
|
holder.bind(parent, listener)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,9 @@ import org.oxycblt.auxio.util.logD
|
||||||
abstract class DetailHeaderAdapter<T : MusicParent, VH : RecyclerView.ViewHolder> :
|
abstract class DetailHeaderAdapter<T : MusicParent, VH : RecyclerView.ViewHolder> :
|
||||||
RecyclerView.Adapter<VH>() {
|
RecyclerView.Adapter<VH>() {
|
||||||
private var currentParent: T? = null
|
private var currentParent: T? = null
|
||||||
|
|
||||||
final override fun getItemCount() = 1
|
final override fun getItemCount() = 1
|
||||||
|
|
||||||
final override fun onBindViewHolder(holder: VH, position: Int) =
|
final override fun onBindViewHolder(holder: VH, position: Int) =
|
||||||
onBindHeader(holder, requireNotNull(currentParent))
|
onBindHeader(holder, requireNotNull(currentParent))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ abstract class DetailListAdapter(
|
||||||
* Called when the button in a [SortHeader] item is pressed, requesting that the sort menu
|
* Called when the button in a [SortHeader] item is pressed, requesting that the sort menu
|
||||||
* should be opened.
|
* should be opened.
|
||||||
*/
|
*/
|
||||||
fun onOpenSortMenu(anchor: View)
|
fun onOpenSortMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected companion object {
|
protected companion object {
|
||||||
|
|
@ -132,7 +132,7 @@ private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
|
||||||
// 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.
|
||||||
TooltipCompat.setTooltipText(this, contentDescription)
|
TooltipCompat.setTooltipText(this, contentDescription)
|
||||||
setOnClickListener(listener::onOpenSortMenu)
|
setOnClickListener { listener.onOpenSortMenu() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -170,10 +170,25 @@ private class EditHeaderViewHolder private constructor(private val binding: Item
|
||||||
TooltipCompat.setTooltipText(this, contentDescription)
|
TooltipCompat.setTooltipText(this, contentDescription)
|
||||||
setOnClickListener { listener.onStartEdit() }
|
setOnClickListener { listener.onStartEdit() }
|
||||||
}
|
}
|
||||||
|
binding.headerSort.apply {
|
||||||
|
TooltipCompat.setTooltipText(this, contentDescription)
|
||||||
|
setOnClickListener { listener.onOpenSortMenu() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateEditing(editing: Boolean) {
|
override fun updateEditing(editing: Boolean) {
|
||||||
binding.headerEdit.isEnabled = !editing
|
binding.headerEdit.apply {
|
||||||
|
isVisible = !editing
|
||||||
|
isClickable = !editing
|
||||||
|
isFocusable = !editing
|
||||||
|
jumpDrawablesToCurrentState()
|
||||||
|
}
|
||||||
|
binding.headerSort.apply {
|
||||||
|
isVisible = editing
|
||||||
|
isClickable = editing
|
||||||
|
isFocusable = editing
|
||||||
|
jumpDrawablesToCurrentState()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
@ -211,6 +226,7 @@ private constructor(private val binding: ItemEditableSongBinding) :
|
||||||
PlaylistDetailListAdapter.ViewHolder {
|
PlaylistDetailListAdapter.ViewHolder {
|
||||||
override val enabled: Boolean
|
override val enabled: Boolean
|
||||||
get() = binding.songDragHandle.isVisible
|
get() = binding.songDragHandle.isVisible
|
||||||
|
|
||||||
override val root = binding.root
|
override val root = binding.root
|
||||||
override val body = binding.body
|
override val body = binding.body
|
||||||
override val delete = binding.background
|
override val delete = binding.background
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* AlbumSongSortDialog.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.detail.sort
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.oxycblt.auxio.databinding.DialogSortBinding
|
||||||
|
import org.oxycblt.auxio.detail.DetailViewModel
|
||||||
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
|
import org.oxycblt.auxio.list.sort.SortDialog
|
||||||
|
import org.oxycblt.auxio.music.Album
|
||||||
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [SortDialog] that controls the [Sort] of [DetailViewModel.albumSongSort].
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class AlbumSongSortDialog : SortDialog() {
|
||||||
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onBindingCreated(binding: DialogSortBinding, savedInstanceState: Bundle?) {
|
||||||
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
|
// --- VIEWMODEL SETUP ---
|
||||||
|
collectImmediately(detailModel.currentAlbum, ::updateAlbum)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInitialSort() = detailModel.albumSongSort
|
||||||
|
|
||||||
|
override fun applyChosenSort(sort: Sort) {
|
||||||
|
detailModel.applyAlbumSongSort(sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getModeChoices() = listOf(Sort.Mode.ByDisc, Sort.Mode.ByTrack)
|
||||||
|
|
||||||
|
private fun updateAlbum(album: Album?) {
|
||||||
|
if (album == null) {
|
||||||
|
logD("No album to sort, navigating away")
|
||||||
|
findNavController().navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* ArtistSongSortDialog.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.detail.sort
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.oxycblt.auxio.databinding.DialogSortBinding
|
||||||
|
import org.oxycblt.auxio.detail.DetailViewModel
|
||||||
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
|
import org.oxycblt.auxio.list.sort.SortDialog
|
||||||
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [SortDialog] that controls the [Sort] of [DetailViewModel.artistSongSort].
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class ArtistSongSortDialog : SortDialog() {
|
||||||
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onBindingCreated(binding: DialogSortBinding, savedInstanceState: Bundle?) {
|
||||||
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
|
// --- VIEWMODEL SETUP ---
|
||||||
|
collectImmediately(detailModel.currentArtist, ::updateArtist)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInitialSort() = detailModel.artistSongSort
|
||||||
|
|
||||||
|
override fun applyChosenSort(sort: Sort) {
|
||||||
|
detailModel.applyArtistSongSort(sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getModeChoices() =
|
||||||
|
listOf(Sort.Mode.ByName, Sort.Mode.ByAlbum, Sort.Mode.ByDate, Sort.Mode.ByDuration)
|
||||||
|
|
||||||
|
private fun updateArtist(artist: Artist?) {
|
||||||
|
if (artist == null) {
|
||||||
|
logD("No artist to sort, navigating away")
|
||||||
|
findNavController().navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* GenreSongSortDialog.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.detail.sort
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.oxycblt.auxio.databinding.DialogSortBinding
|
||||||
|
import org.oxycblt.auxio.detail.DetailViewModel
|
||||||
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
|
import org.oxycblt.auxio.list.sort.SortDialog
|
||||||
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [SortDialog] that controls the [Sort] of [DetailViewModel.genreSongSort].
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class GenreSongSortDialog : SortDialog() {
|
||||||
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onBindingCreated(binding: DialogSortBinding, savedInstanceState: Bundle?) {
|
||||||
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
|
// --- VIEWMODEL SETUP ---
|
||||||
|
collectImmediately(detailModel.currentGenre, ::updateGenre)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInitialSort() = detailModel.genreSongSort
|
||||||
|
|
||||||
|
override fun applyChosenSort(sort: Sort) {
|
||||||
|
detailModel.applyGenreSongSort(sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getModeChoices() =
|
||||||
|
listOf(
|
||||||
|
Sort.Mode.ByName,
|
||||||
|
Sort.Mode.ByArtist,
|
||||||
|
Sort.Mode.ByAlbum,
|
||||||
|
Sort.Mode.ByDate,
|
||||||
|
Sort.Mode.ByDuration)
|
||||||
|
|
||||||
|
private fun updateGenre(genre: Genre?) {
|
||||||
|
if (genre == null) {
|
||||||
|
logD("No genre to sort, navigating away")
|
||||||
|
findNavController().navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* PlaylistSongSortDialog.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.detail.sort
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.oxycblt.auxio.databinding.DialogSortBinding
|
||||||
|
import org.oxycblt.auxio.detail.DetailViewModel
|
||||||
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
|
import org.oxycblt.auxio.list.sort.SortDialog
|
||||||
|
import org.oxycblt.auxio.music.Playlist
|
||||||
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [SortDialog] that controls the [Sort] of [DetailViewModel.genreSongSort].
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class PlaylistSongSortDialog : SortDialog() {
|
||||||
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onBindingCreated(binding: DialogSortBinding, savedInstanceState: Bundle?) {
|
||||||
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
|
// --- VIEWMODEL SETUP ---
|
||||||
|
collectImmediately(detailModel.currentPlaylist, ::updatePlaylist)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInitialSort() = null
|
||||||
|
|
||||||
|
override fun applyChosenSort(sort: Sort) {
|
||||||
|
detailModel.applyPlaylistSongSort(sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getModeChoices() =
|
||||||
|
listOf(
|
||||||
|
Sort.Mode.ByName,
|
||||||
|
Sort.Mode.ByArtist,
|
||||||
|
Sort.Mode.ByAlbum,
|
||||||
|
Sort.Mode.ByDate,
|
||||||
|
Sort.Mode.ByDuration)
|
||||||
|
|
||||||
|
private fun updatePlaylist(genre: Playlist?) {
|
||||||
|
if (genre == null) {
|
||||||
|
logD("No genre to sort, navigating away")
|
||||||
|
findNavController().navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,7 +26,6 @@ import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.view.MenuCompat
|
import androidx.core.view.MenuCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.iterator
|
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
|
|
@ -38,7 +37,6 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import com.google.android.material.transition.MaterialFadeThrough
|
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
|
|
@ -56,13 +54,12 @@ import org.oxycblt.auxio.home.list.SongListFragment
|
||||||
import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy
|
import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy
|
||||||
import org.oxycblt.auxio.home.tabs.Tab
|
import org.oxycblt.auxio.home.tabs.Tab
|
||||||
import org.oxycblt.auxio.list.ListViewModel
|
import org.oxycblt.auxio.list.ListViewModel
|
||||||
import org.oxycblt.auxio.list.Menu
|
|
||||||
import org.oxycblt.auxio.list.SelectionFragment
|
import org.oxycblt.auxio.list.SelectionFragment
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.menu.Menu
|
||||||
import org.oxycblt.auxio.music.IndexingProgress
|
import org.oxycblt.auxio.music.IndexingProgress
|
||||||
import org.oxycblt.auxio.music.IndexingState
|
import org.oxycblt.auxio.music.IndexingState
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicType
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.NoAudioPermissionException
|
import org.oxycblt.auxio.music.NoAudioPermissionException
|
||||||
import org.oxycblt.auxio.music.NoMusicException
|
import org.oxycblt.auxio.music.NoMusicException
|
||||||
|
|
@ -77,7 +74,6 @@ import org.oxycblt.auxio.util.lazyReflectedField
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
import org.oxycblt.auxio.util.navigateSafe
|
import org.oxycblt.auxio.util.navigateSafe
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The starting [SelectionFragment] of Auxio. Shows the user's music library and enables navigation
|
* The starting [SelectionFragment] of Auxio. Shows the user's music library and enables navigation
|
||||||
|
|
@ -102,10 +98,9 @@ class HomeFragment :
|
||||||
// Orientation change will wipe whatever transition we were using prior, which will
|
// Orientation change will wipe whatever transition we were using prior, which will
|
||||||
// result in no transition when the user navigates back. Make sure we re-initialize
|
// result in no transition when the user navigates back. Make sure we re-initialize
|
||||||
// our transitions.
|
// our transitions.
|
||||||
when (val id = savedInstanceState.getInt(KEY_LAST_TRANSITION_ID, -2)) {
|
val axis = savedInstanceState.getInt(KEY_LAST_TRANSITION_ID, -1)
|
||||||
-2 -> {}
|
if (axis > -1) {
|
||||||
-1 -> applyFadeTransition()
|
applyAxisTransition(axis)
|
||||||
else -> applyAxisTransition(id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -173,8 +168,8 @@ class HomeFragment :
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
collect(homeModel.recreateTabs.flow, ::handleRecreate)
|
collect(homeModel.recreateTabs.flow, ::handleRecreate)
|
||||||
collectImmediately(homeModel.currentTabMode, ::updateCurrentTab)
|
collectImmediately(homeModel.currentTabType, ::updateCurrentTab)
|
||||||
collectImmediately(homeModel.songsList, homeModel.isFastScrolling, ::updateFab)
|
collectImmediately(homeModel.songList, homeModel.isFastScrolling, ::updateFab)
|
||||||
collect(listModel.menu.flow, ::handleMenu)
|
collect(listModel.menu.flow, ::handleMenu)
|
||||||
collectImmediately(listModel.selected, ::updateSelection)
|
collectImmediately(listModel.selected, ::updateSelection)
|
||||||
collectImmediately(musicModel.indexingState, ::updateIndexerState)
|
collectImmediately(musicModel.indexingState, ::updateIndexerState)
|
||||||
|
|
@ -183,9 +178,9 @@ class HomeFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
when (val transition = enterTransition) {
|
val transition = enterTransition
|
||||||
is MaterialFadeThrough -> outState.putInt(KEY_LAST_TRANSITION_ID, -1)
|
if (transition is MaterialSharedAxis) {
|
||||||
is MaterialSharedAxis -> outState.putInt(KEY_LAST_TRANSITION_ID, transition.axis)
|
outState.putInt(KEY_LAST_TRANSITION_ID, transition.axis)
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
|
|
@ -224,63 +219,42 @@ class HomeFragment :
|
||||||
}
|
}
|
||||||
R.id.action_settings -> {
|
R.id.action_settings -> {
|
||||||
logD("Navigating to preferences")
|
logD("Navigating to preferences")
|
||||||
applyFadeTransition()
|
homeModel.showSettings()
|
||||||
findNavController().navigateSafe(HomeFragmentDirections.preferences())
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.action_about -> {
|
R.id.action_about -> {
|
||||||
logD("Navigating to about")
|
logD("Navigating to about")
|
||||||
applyFadeTransition()
|
homeModel.showAbout()
|
||||||
findNavController().navigateSafe(HomeFragmentDirections.about())
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle sort menu
|
// Handle sort menu
|
||||||
R.id.submenu_sorting -> {
|
R.id.action_sort -> {
|
||||||
// Junk click event when opening the menu
|
// Junk click event when opening the menu
|
||||||
true
|
val directions =
|
||||||
|
when (homeModel.currentTabType.value) {
|
||||||
|
MusicType.SONGS -> HomeFragmentDirections.sortSongs()
|
||||||
|
MusicType.ALBUMS -> HomeFragmentDirections.sortAlbums()
|
||||||
|
MusicType.ARTISTS -> HomeFragmentDirections.sortArtists()
|
||||||
|
MusicType.GENRES -> HomeFragmentDirections.sortGenres()
|
||||||
|
MusicType.PLAYLISTS -> HomeFragmentDirections.sortPlaylists()
|
||||||
}
|
}
|
||||||
R.id.option_sort_asc -> {
|
findNavController().navigateSafe(directions)
|
||||||
logD("Switching to ascending sorting")
|
|
||||||
item.isChecked = true
|
|
||||||
homeModel.setSortForCurrentTab(
|
|
||||||
homeModel
|
|
||||||
.getSortForTab(homeModel.currentTabMode.value)
|
|
||||||
.withDirection(Sort.Direction.ASCENDING))
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.option_sort_dec -> {
|
|
||||||
logD("Switching to descending sorting")
|
|
||||||
item.isChecked = true
|
|
||||||
homeModel.setSortForCurrentTab(
|
|
||||||
homeModel
|
|
||||||
.getSortForTab(homeModel.currentTabMode.value)
|
|
||||||
.withDirection(Sort.Direction.DESCENDING))
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val newMode = Sort.Mode.fromItemId(item.itemId)
|
|
||||||
if (newMode != null) {
|
|
||||||
// Sorting option was selected, mark it as selected and update the mode
|
|
||||||
logD("Updating sort mode")
|
|
||||||
item.isChecked = true
|
|
||||||
homeModel.setSortForCurrentTab(
|
|
||||||
homeModel.getSortForTab(homeModel.currentTabMode.value).withMode(newMode))
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
logW("Unexpected menu item selected")
|
logW("Unexpected menu item selected")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupPager(binding: FragmentHomeBinding) {
|
private fun setupPager(binding: FragmentHomeBinding) {
|
||||||
binding.homePager.adapter =
|
binding.homePager.adapter =
|
||||||
HomePagerAdapter(homeModel.currentTabModes, childFragmentManager, viewLifecycleOwner)
|
HomePagerAdapter(homeModel.currentTabTypes, childFragmentManager, viewLifecycleOwner)
|
||||||
|
|
||||||
val toolbarParams = binding.homeToolbar.layoutParams as AppBarLayout.LayoutParams
|
val toolbarParams = binding.homeToolbar.layoutParams as AppBarLayout.LayoutParams
|
||||||
if (homeModel.currentTabModes.size == 1) {
|
if (homeModel.currentTabTypes.size == 1) {
|
||||||
// A single tab makes the tab layout redundant, hide it and disable the collapsing
|
// A single tab makes the tab layout redundant, hide it and disable the collapsing
|
||||||
// behavior.
|
// behavior.
|
||||||
logD("Single tab shown, disabling TabLayout")
|
logD("Single tab shown, disabling TabLayout")
|
||||||
|
|
@ -298,81 +272,26 @@ class HomeFragment :
|
||||||
TabLayoutMediator(
|
TabLayoutMediator(
|
||||||
binding.homeTabs,
|
binding.homeTabs,
|
||||||
binding.homePager,
|
binding.homePager,
|
||||||
AdaptiveTabStrategy(requireContext(), homeModel.currentTabModes))
|
AdaptiveTabStrategy(requireContext(), homeModel.currentTabTypes))
|
||||||
.attach()
|
.attach()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateCurrentTab(tabMode: MusicMode) {
|
private fun updateCurrentTab(tabType: MusicType) {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
// Update the sort options to align with those allowed by the tab
|
|
||||||
val isVisible: (Int) -> Boolean =
|
|
||||||
when (tabMode) {
|
|
||||||
// Disallow sorting by count for songs
|
|
||||||
MusicMode.SONGS -> {
|
|
||||||
logD("Using song-specific menu options")
|
|
||||||
({ id -> id != R.id.option_sort_count })
|
|
||||||
}
|
|
||||||
// Disallow sorting by album for albums
|
|
||||||
MusicMode.ALBUMS -> {
|
|
||||||
logD("Using album-specific menu options")
|
|
||||||
({ id -> id != R.id.option_sort_album })
|
|
||||||
}
|
|
||||||
// Only allow sorting by name, count, and duration for parents
|
|
||||||
else -> {
|
|
||||||
logD("Using parent-specific menu options")
|
|
||||||
({ id ->
|
|
||||||
id == R.id.option_sort_asc ||
|
|
||||||
id == R.id.option_sort_dec ||
|
|
||||||
id == R.id.option_sort_name ||
|
|
||||||
id == R.id.option_sort_count ||
|
|
||||||
id == R.id.option_sort_duration
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val sortMenu =
|
|
||||||
unlikelyToBeNull(binding.homeNormalToolbar.menu.findItem(R.id.submenu_sorting).subMenu)
|
|
||||||
val toHighlight = homeModel.getSortForTab(tabMode)
|
|
||||||
|
|
||||||
for (option in sortMenu) {
|
|
||||||
val isCurrentMode = option.itemId == toHighlight.mode.itemId
|
|
||||||
val isCurrentlyAscending =
|
|
||||||
option.itemId == R.id.option_sort_asc &&
|
|
||||||
toHighlight.direction == Sort.Direction.ASCENDING
|
|
||||||
val isCurrentlyDescending =
|
|
||||||
option.itemId == R.id.option_sort_dec &&
|
|
||||||
toHighlight.direction == Sort.Direction.DESCENDING
|
|
||||||
// Check the corresponding direction and mode sort options to align with
|
|
||||||
// the current sort of the tab.
|
|
||||||
if (isCurrentMode || isCurrentlyAscending || isCurrentlyDescending) {
|
|
||||||
logD(
|
|
||||||
"Checking $option option [mode: $isCurrentMode asc: $isCurrentlyAscending dec: $isCurrentlyDescending]")
|
|
||||||
// Note: We cannot inline this boolean assignment since it unchecks all other radio
|
|
||||||
// buttons (even when setting it to false), which would result in nothing being
|
|
||||||
// selected.
|
|
||||||
option.isChecked = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable options that are not allowed by the isVisible lambda
|
|
||||||
option.isVisible = isVisible(option.itemId)
|
|
||||||
if (!option.isVisible) {
|
|
||||||
logD("Hiding $option option")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the scrolling view in AppBarLayout to align with the current tab's
|
// Update the scrolling view in AppBarLayout to align with the current tab's
|
||||||
// scrolling state. This prevents the lift state from being confused as one
|
// scrolling state. This prevents the lift state from being confused as one
|
||||||
// goes between different tabs.
|
// goes between different tabs.
|
||||||
binding.homeAppbar.liftOnScrollTargetViewId =
|
binding.homeAppbar.liftOnScrollTargetViewId =
|
||||||
when (tabMode) {
|
when (tabType) {
|
||||||
MusicMode.SONGS -> R.id.home_song_recycler
|
MusicType.SONGS -> R.id.home_song_recycler
|
||||||
MusicMode.ALBUMS -> R.id.home_album_recycler
|
MusicType.ALBUMS -> R.id.home_album_recycler
|
||||||
MusicMode.ARTISTS -> R.id.home_artist_recycler
|
MusicType.ARTISTS -> R.id.home_artist_recycler
|
||||||
MusicMode.GENRES -> R.id.home_genre_recycler
|
MusicType.GENRES -> R.id.home_genre_recycler
|
||||||
MusicMode.PLAYLISTS -> R.id.home_playlist_recycler
|
MusicType.PLAYLISTS -> R.id.home_playlist_recycler
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tabMode != MusicMode.PLAYLISTS) {
|
if (tabType != MusicType.PLAYLISTS) {
|
||||||
logD("Flipping to shuffle button")
|
logD("Flipping to shuffle button")
|
||||||
binding.homeFab.flipTo(R.drawable.ic_shuffle_off_24, R.string.desc_shuffle_all) {
|
binding.homeFab.flipTo(R.drawable.ic_shuffle_off_24, R.string.desc_shuffle_all) {
|
||||||
playbackModel.shuffleAll()
|
playbackModel.shuffleAll()
|
||||||
|
|
@ -577,15 +496,11 @@ class HomeFragment :
|
||||||
if (menu == null) return
|
if (menu == null) return
|
||||||
val directions =
|
val directions =
|
||||||
when (menu) {
|
when (menu) {
|
||||||
is Menu.ForSong -> HomeFragmentDirections.openSongMenu(menu.menuRes, menu.music.uid)
|
is Menu.ForSong -> HomeFragmentDirections.openSongMenu(menu.parcel)
|
||||||
is Menu.ForAlbum ->
|
is Menu.ForAlbum -> HomeFragmentDirections.openAlbumMenu(menu.parcel)
|
||||||
HomeFragmentDirections.openAlbumMenu(menu.menuRes, menu.music.uid)
|
is Menu.ForArtist -> HomeFragmentDirections.openArtistMenu(menu.parcel)
|
||||||
is Menu.ForArtist ->
|
is Menu.ForGenre -> HomeFragmentDirections.openGenreMenu(menu.parcel)
|
||||||
HomeFragmentDirections.openArtistMenu(menu.menuRes, menu.music.uid)
|
is Menu.ForPlaylist -> HomeFragmentDirections.openPlaylistMenu(menu.parcel)
|
||||||
is Menu.ForGenre ->
|
|
||||||
HomeFragmentDirections.openGenreMenu(menu.menuRes, menu.music.uid)
|
|
||||||
is Menu.ForPlaylist ->
|
|
||||||
HomeFragmentDirections.openPlaylistMenu(menu.menuRes, menu.music.uid)
|
|
||||||
}
|
}
|
||||||
findNavController().navigateSafe(directions)
|
findNavController().navigateSafe(directions)
|
||||||
}
|
}
|
||||||
|
|
@ -616,13 +531,6 @@ class HomeFragment :
|
||||||
reenterTransition = MaterialSharedAxis(axis, false)
|
reenterTransition = MaterialSharedAxis(axis, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyFadeTransition() {
|
|
||||||
enterTransition = MaterialFadeThrough()
|
|
||||||
returnTransition = MaterialFadeThrough()
|
|
||||||
exitTransition = MaterialFadeThrough()
|
|
||||||
reenterTransition = MaterialFadeThrough()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [FragmentStateAdapter] implementation for the [HomeFragment]'s [ViewPager2] instance.
|
* [FragmentStateAdapter] implementation for the [HomeFragment]'s [ViewPager2] instance.
|
||||||
*
|
*
|
||||||
|
|
@ -632,18 +540,19 @@ class HomeFragment :
|
||||||
* [FragmentStateAdapter].
|
* [FragmentStateAdapter].
|
||||||
*/
|
*/
|
||||||
private class HomePagerAdapter(
|
private class HomePagerAdapter(
|
||||||
private val tabs: List<MusicMode>,
|
private val tabs: List<MusicType>,
|
||||||
fragmentManager: FragmentManager,
|
fragmentManager: FragmentManager,
|
||||||
lifecycleOwner: LifecycleOwner
|
lifecycleOwner: LifecycleOwner
|
||||||
) : FragmentStateAdapter(fragmentManager, lifecycleOwner.lifecycle) {
|
) : FragmentStateAdapter(fragmentManager, lifecycleOwner.lifecycle) {
|
||||||
override fun getItemCount() = tabs.size
|
override fun getItemCount() = tabs.size
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment =
|
override fun createFragment(position: Int): Fragment =
|
||||||
when (tabs[position]) {
|
when (tabs[position]) {
|
||||||
MusicMode.SONGS -> SongListFragment()
|
MusicType.SONGS -> SongListFragment()
|
||||||
MusicMode.ALBUMS -> AlbumListFragment()
|
MusicType.ALBUMS -> AlbumListFragment()
|
||||||
MusicMode.ARTISTS -> ArtistListFragment()
|
MusicType.ARTISTS -> ArtistListFragment()
|
||||||
MusicMode.GENRES -> GenreListFragment()
|
MusicType.GENRES -> GenreListFragment()
|
||||||
MusicMode.PLAYLISTS -> PlaylistListFragment()
|
MusicType.PLAYLISTS -> PlaylistListFragment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.home.tabs.Tab
|
import org.oxycblt.auxio.home.tabs.Tab
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicType
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
@ -75,9 +75,9 @@ class HomeSettingsImpl @Inject constructor(@ApplicationContext context: Context)
|
||||||
logD("Old tabs: $oldTabs")
|
logD("Old tabs: $oldTabs")
|
||||||
|
|
||||||
// The playlist tab is now parsed, but it needs to be made visible.
|
// The playlist tab is now parsed, but it needs to be made visible.
|
||||||
val playlistIndex = oldTabs.indexOfFirst { it.mode == MusicMode.PLAYLISTS }
|
val playlistIndex = oldTabs.indexOfFirst { it.type == MusicType.PLAYLISTS }
|
||||||
check(playlistIndex > -1) // This should exist, otherwise we are in big trouble
|
check(playlistIndex > -1) // This should exist, otherwise we are in big trouble
|
||||||
oldTabs[playlistIndex] = Tab.Visible(MusicMode.PLAYLISTS)
|
oldTabs[playlistIndex] = Tab.Visible(MusicType.PLAYLISTS)
|
||||||
logD("New tabs: $oldTabs")
|
logD("New tabs: $oldTabs")
|
||||||
|
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,17 @@ import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.oxycblt.auxio.home.tabs.Tab
|
import org.oxycblt.auxio.home.tabs.Tab
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.ListSettings
|
||||||
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||||
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
import org.oxycblt.auxio.music.MusicType
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.playback.PlaySong
|
||||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
import org.oxycblt.auxio.util.Event
|
import org.oxycblt.auxio.util.Event
|
||||||
import org.oxycblt.auxio.util.MutableEvent
|
import org.oxycblt.auxio.util.MutableEvent
|
||||||
|
|
@ -49,73 +50,98 @@ class HomeViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val homeSettings: HomeSettings,
|
private val homeSettings: HomeSettings,
|
||||||
|
private val listSettings: ListSettings,
|
||||||
private val playbackSettings: PlaybackSettings,
|
private val playbackSettings: PlaybackSettings,
|
||||||
private val musicRepository: MusicRepository,
|
private val musicRepository: MusicRepository,
|
||||||
private val musicSettings: MusicSettings
|
|
||||||
) : ViewModel(), MusicRepository.UpdateListener, HomeSettings.Listener {
|
) : ViewModel(), MusicRepository.UpdateListener, HomeSettings.Listener {
|
||||||
|
|
||||||
private val _songsList = MutableStateFlow(listOf<Song>())
|
private val _songList = MutableStateFlow(listOf<Song>())
|
||||||
/** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */
|
/** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */
|
||||||
val songsList: StateFlow<List<Song>>
|
val songList: StateFlow<List<Song>>
|
||||||
get() = _songsList
|
get() = _songList
|
||||||
private val _songsInstructions = MutableEvent<UpdateInstructions>()
|
|
||||||
/** Instructions for how to update [songsList] in the UI. */
|
|
||||||
val songsInstructions: Event<UpdateInstructions>
|
|
||||||
get() = _songsInstructions
|
|
||||||
|
|
||||||
private val _albumsLists = MutableStateFlow(listOf<Album>())
|
private val _songInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
/** Instructions for how to update [songList] in the UI. */
|
||||||
|
val songInstructions: Event<UpdateInstructions>
|
||||||
|
get() = _songInstructions
|
||||||
|
|
||||||
|
/** The current [Sort] used for [songList]. */
|
||||||
|
val songSort: Sort
|
||||||
|
get() = listSettings.songSort
|
||||||
|
|
||||||
|
/** The [PlaySong] instructions to use when playing a [Song]. */
|
||||||
|
val playWith
|
||||||
|
get() = playbackSettings.playInListWith
|
||||||
|
|
||||||
|
private val _albumList = MutableStateFlow(listOf<Album>())
|
||||||
/** A list of [Album]s, sorted by the preferred [Sort], to be shown in the home view. */
|
/** A list of [Album]s, sorted by the preferred [Sort], to be shown in the home view. */
|
||||||
val albumsList: StateFlow<List<Album>>
|
val albumList: StateFlow<List<Album>>
|
||||||
get() = _albumsLists
|
get() = _albumList
|
||||||
private val _albumsInstructions = MutableEvent<UpdateInstructions>()
|
|
||||||
/** Instructions for how to update [albumsList] in the UI. */
|
|
||||||
val albumsInstructions: Event<UpdateInstructions>
|
|
||||||
get() = _albumsInstructions
|
|
||||||
|
|
||||||
private val _artistsList = MutableStateFlow(listOf<Artist>())
|
private val _albumInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
/** Instructions for how to update [albumList] in the UI. */
|
||||||
|
val albumInstructions: Event<UpdateInstructions>
|
||||||
|
get() = _albumInstructions
|
||||||
|
|
||||||
|
/** The current [Sort] used for [albumList]. */
|
||||||
|
val albumSort: Sort
|
||||||
|
get() = listSettings.albumSort
|
||||||
|
|
||||||
|
private val _artistList = MutableStateFlow(listOf<Artist>())
|
||||||
/**
|
/**
|
||||||
* A list of [Artist]s, sorted by the preferred [Sort], to be shown in the home view. Note that
|
* A list of [Artist]s, sorted by the preferred [Sort], to be shown in the home view. Note that
|
||||||
* if "Hide collaborators" is on, this list will not include collaborator [Artist]s.
|
* if "Hide collaborators" is on, this list will not include collaborator [Artist]s.
|
||||||
*/
|
*/
|
||||||
val artistsList: MutableStateFlow<List<Artist>>
|
val artistList: MutableStateFlow<List<Artist>>
|
||||||
get() = _artistsList
|
get() = _artistList
|
||||||
private val _artistsInstructions = MutableEvent<UpdateInstructions>()
|
|
||||||
/** Instructions for how to update [artistsList] in the UI. */
|
|
||||||
val artistsInstructions: Event<UpdateInstructions>
|
|
||||||
get() = _artistsInstructions
|
|
||||||
|
|
||||||
private val _genresList = MutableStateFlow(listOf<Genre>())
|
private val _artistInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
/** Instructions for how to update [artistList] in the UI. */
|
||||||
|
val artistInstructions: Event<UpdateInstructions>
|
||||||
|
get() = _artistInstructions
|
||||||
|
|
||||||
|
/** The current [Sort] used for [artistList]. */
|
||||||
|
val artistSort: Sort
|
||||||
|
get() = listSettings.artistSort
|
||||||
|
|
||||||
|
private val _genreList = MutableStateFlow(listOf<Genre>())
|
||||||
/** A list of [Genre]s, sorted by the preferred [Sort], to be shown in the home view. */
|
/** A list of [Genre]s, sorted by the preferred [Sort], to be shown in the home view. */
|
||||||
val genresList: StateFlow<List<Genre>>
|
val genreList: StateFlow<List<Genre>>
|
||||||
get() = _genresList
|
get() = _genreList
|
||||||
private val _genresInstructions = MutableEvent<UpdateInstructions>()
|
|
||||||
/** Instructions for how to update [genresList] in the UI. */
|
|
||||||
val genresInstructions: Event<UpdateInstructions>
|
|
||||||
get() = _genresInstructions
|
|
||||||
|
|
||||||
private val _playlistsList = MutableStateFlow(listOf<Playlist>())
|
private val _genreInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
/** Instructions for how to update [genreList] in the UI. */
|
||||||
|
val genreInstructions: Event<UpdateInstructions>
|
||||||
|
get() = _genreInstructions
|
||||||
|
|
||||||
|
/** The current [Sort] used for [genreList]. */
|
||||||
|
val genreSort: Sort
|
||||||
|
get() = listSettings.genreSort
|
||||||
|
|
||||||
|
private val _playlistList = MutableStateFlow(listOf<Playlist>())
|
||||||
/** A list of [Playlist]s, sorted by the preferred [Sort], to be shown in the home view. */
|
/** A list of [Playlist]s, sorted by the preferred [Sort], to be shown in the home view. */
|
||||||
val playlistsList: StateFlow<List<Playlist>>
|
val playlistList: StateFlow<List<Playlist>>
|
||||||
get() = _playlistsList
|
get() = _playlistList
|
||||||
private val _playlistsInstructions = MutableEvent<UpdateInstructions>()
|
|
||||||
/** Instructions for how to update [genresList] in the UI. */
|
|
||||||
val playlistsInstructions: Event<UpdateInstructions>
|
|
||||||
get() = _playlistsInstructions
|
|
||||||
|
|
||||||
/** The [MusicMode] to use when playing a [Song] from the UI. */
|
private val _playlistInstructions = MutableEvent<UpdateInstructions>()
|
||||||
val playbackMode: MusicMode
|
/** Instructions for how to update [genreList] in the UI. */
|
||||||
get() = playbackSettings.inListPlaybackMode
|
val playlistInstructions: Event<UpdateInstructions>
|
||||||
|
get() = _playlistInstructions
|
||||||
|
|
||||||
|
/** The current [Sort] used for [genreList]. */
|
||||||
|
val playlistSort: Sort
|
||||||
|
get() = listSettings.playlistSort
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of [MusicMode] corresponding to the current [Tab] configuration, excluding invisible
|
* A list of [MusicType] corresponding to the current [Tab] configuration, excluding invisible
|
||||||
* [Tab]s.
|
* [Tab]s.
|
||||||
*/
|
*/
|
||||||
var currentTabModes = makeTabModes()
|
var currentTabTypes = makeTabTypes()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private val _currentTabMode = MutableStateFlow(currentTabModes[0])
|
private val _currentTabType = MutableStateFlow(currentTabTypes[0])
|
||||||
/** The [MusicMode] of the currently shown [Tab]. */
|
/** The [MusicType] of the currently shown [Tab]. */
|
||||||
val currentTabMode: StateFlow<MusicMode> = _currentTabMode
|
val currentTabType: StateFlow<MusicType> = _currentTabType
|
||||||
|
|
||||||
private val _shouldRecreate = MutableEvent<Unit>()
|
private val _shouldRecreate = MutableEvent<Unit>()
|
||||||
/**
|
/**
|
||||||
|
|
@ -130,6 +156,10 @@ constructor(
|
||||||
/** A marker for whether the user is fast-scrolling in the home view or not. */
|
/** A marker for whether the user is fast-scrolling in the home view or not. */
|
||||||
val isFastScrolling: StateFlow<Boolean> = _isFastScrolling
|
val isFastScrolling: StateFlow<Boolean> = _isFastScrolling
|
||||||
|
|
||||||
|
private val _showOuter = MutableEvent<Outer>()
|
||||||
|
val showOuter: Event<Outer>
|
||||||
|
get() = _showOuter
|
||||||
|
|
||||||
init {
|
init {
|
||||||
musicRepository.addUpdateListener(this)
|
musicRepository.addUpdateListener(this)
|
||||||
homeSettings.registerListener(this)
|
homeSettings.registerListener(this)
|
||||||
|
|
@ -147,13 +177,13 @@ constructor(
|
||||||
logD("Refreshing library")
|
logD("Refreshing library")
|
||||||
// Get the each list of items in the library to use as our list data.
|
// Get the each list of items in the library to use as our list data.
|
||||||
// Applying the preferred sorting to them.
|
// Applying the preferred sorting to them.
|
||||||
_songsInstructions.put(UpdateInstructions.Diff)
|
_songInstructions.put(UpdateInstructions.Diff)
|
||||||
_songsList.value = musicSettings.songSort.songs(deviceLibrary.songs)
|
_songList.value = listSettings.songSort.songs(deviceLibrary.songs)
|
||||||
_albumsInstructions.put(UpdateInstructions.Diff)
|
_albumInstructions.put(UpdateInstructions.Diff)
|
||||||
_albumsLists.value = musicSettings.albumSort.albums(deviceLibrary.albums)
|
_albumList.value = listSettings.albumSort.albums(deviceLibrary.albums)
|
||||||
_artistsInstructions.put(UpdateInstructions.Diff)
|
_artistInstructions.put(UpdateInstructions.Diff)
|
||||||
_artistsList.value =
|
_artistList.value =
|
||||||
musicSettings.artistSort.artists(
|
listSettings.artistSort.artists(
|
||||||
if (homeSettings.shouldHideCollaborators) {
|
if (homeSettings.shouldHideCollaborators) {
|
||||||
logD("Filtering collaborator artists")
|
logD("Filtering collaborator artists")
|
||||||
// Hide Collaborators is enabled, filter out collaborators.
|
// Hide Collaborators is enabled, filter out collaborators.
|
||||||
|
|
@ -162,22 +192,22 @@ constructor(
|
||||||
logD("Using all artists")
|
logD("Using all artists")
|
||||||
deviceLibrary.artists
|
deviceLibrary.artists
|
||||||
})
|
})
|
||||||
_genresInstructions.put(UpdateInstructions.Diff)
|
_genreInstructions.put(UpdateInstructions.Diff)
|
||||||
_genresList.value = musicSettings.genreSort.genres(deviceLibrary.genres)
|
_genreList.value = listSettings.genreSort.genres(deviceLibrary.genres)
|
||||||
}
|
}
|
||||||
|
|
||||||
val userLibrary = musicRepository.userLibrary
|
val userLibrary = musicRepository.userLibrary
|
||||||
if (changes.userLibrary && userLibrary != null) {
|
if (changes.userLibrary && userLibrary != null) {
|
||||||
logD("Refreshing playlists")
|
logD("Refreshing playlists")
|
||||||
_playlistsInstructions.put(UpdateInstructions.Diff)
|
_playlistInstructions.put(UpdateInstructions.Diff)
|
||||||
_playlistsList.value = musicSettings.playlistSort.playlists(userLibrary.playlists)
|
_playlistList.value = listSettings.playlistSort.playlists(userLibrary.playlists)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTabsChanged() {
|
override fun onTabsChanged() {
|
||||||
// Tabs changed, update the current tabs and set up a re-create event.
|
// Tabs changed, update the current tabs and set up a re-create event.
|
||||||
currentTabModes = makeTabModes()
|
currentTabTypes = makeTabTypes()
|
||||||
logD("Updating tabs: ${currentTabMode.value}")
|
logD("Updating tabs: ${currentTabType.value}")
|
||||||
_shouldRecreate.put(Unit)
|
_shouldRecreate.put(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,69 +219,68 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the preferred [Sort] for a given [Tab].
|
* Apply a new [Sort] to [songList].
|
||||||
*
|
*
|
||||||
* @param tabMode The [MusicMode] of the [Tab] desired.
|
* @param sort The [Sort] to apply.
|
||||||
* @return The [Sort] preferred for that [Tab]
|
|
||||||
*/
|
*/
|
||||||
fun getSortForTab(tabMode: MusicMode) =
|
fun applySongSort(sort: Sort) {
|
||||||
when (tabMode) {
|
listSettings.songSort = sort
|
||||||
MusicMode.SONGS -> musicSettings.songSort
|
_songInstructions.put(UpdateInstructions.Replace(0))
|
||||||
MusicMode.ALBUMS -> musicSettings.albumSort
|
_songList.value = listSettings.songSort.songs(_songList.value)
|
||||||
MusicMode.ARTISTS -> musicSettings.artistSort
|
|
||||||
MusicMode.GENRES -> musicSettings.genreSort
|
|
||||||
MusicMode.PLAYLISTS -> musicSettings.playlistSort
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the preferred [Sort] for the current [Tab]. Will update corresponding list.
|
* Apply a new [Sort] to [albumList].
|
||||||
*
|
*
|
||||||
* @param sort The new [Sort] to apply. Assumed to be an allowed sort for the current [Tab].
|
* @param sort The [Sort] to apply.
|
||||||
*/
|
*/
|
||||||
fun setSortForCurrentTab(sort: Sort) {
|
fun applyAlbumSort(sort: Sort) {
|
||||||
// Can simply re-sort the current list of items without having to access the library.
|
listSettings.albumSort = sort
|
||||||
when (val mode = _currentTabMode.value) {
|
_albumInstructions.put(UpdateInstructions.Replace(0))
|
||||||
MusicMode.SONGS -> {
|
_albumList.value = listSettings.albumSort.albums(_albumList.value)
|
||||||
logD("Updating song [$mode] sort mode to $sort")
|
|
||||||
musicSettings.songSort = sort
|
|
||||||
_songsInstructions.put(UpdateInstructions.Replace(0))
|
|
||||||
_songsList.value = sort.songs(_songsList.value)
|
|
||||||
}
|
|
||||||
MusicMode.ALBUMS -> {
|
|
||||||
logD("Updating album [$mode] sort mode to $sort")
|
|
||||||
musicSettings.albumSort = sort
|
|
||||||
_albumsInstructions.put(UpdateInstructions.Replace(0))
|
|
||||||
_albumsLists.value = sort.albums(_albumsLists.value)
|
|
||||||
}
|
|
||||||
MusicMode.ARTISTS -> {
|
|
||||||
logD("Updating artist [$mode] sort mode to $sort")
|
|
||||||
musicSettings.artistSort = sort
|
|
||||||
_artistsInstructions.put(UpdateInstructions.Replace(0))
|
|
||||||
_artistsList.value = sort.artists(_artistsList.value)
|
|
||||||
}
|
|
||||||
MusicMode.GENRES -> {
|
|
||||||
logD("Updating genre [$mode] sort mode to $sort")
|
|
||||||
musicSettings.genreSort = sort
|
|
||||||
_genresInstructions.put(UpdateInstructions.Replace(0))
|
|
||||||
_genresList.value = sort.genres(_genresList.value)
|
|
||||||
}
|
|
||||||
MusicMode.PLAYLISTS -> {
|
|
||||||
logD("Updating playlist [$mode] sort mode to $sort")
|
|
||||||
musicSettings.playlistSort = sort
|
|
||||||
_playlistsInstructions.put(UpdateInstructions.Replace(0))
|
|
||||||
_playlistsList.value = sort.playlists(_playlistsList.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update [currentTabMode] to reflect a new ViewPager2 position
|
* Apply a new [Sort] to [artistList].
|
||||||
|
*
|
||||||
|
* @param sort The [Sort] to apply.
|
||||||
|
*/
|
||||||
|
fun applyArtistSort(sort: Sort) {
|
||||||
|
listSettings.artistSort = sort
|
||||||
|
_artistInstructions.put(UpdateInstructions.Replace(0))
|
||||||
|
_artistList.value = listSettings.artistSort.artists(_artistList.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a new [Sort] to [genreList].
|
||||||
|
*
|
||||||
|
* @param sort The [Sort] to apply.
|
||||||
|
*/
|
||||||
|
fun applyGenreSort(sort: Sort) {
|
||||||
|
listSettings.genreSort = sort
|
||||||
|
_genreInstructions.put(UpdateInstructions.Replace(0))
|
||||||
|
_genreList.value = listSettings.genreSort.genres(_genreList.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a new [Sort] to [playlistList].
|
||||||
|
*
|
||||||
|
* @param sort The [Sort] to apply.
|
||||||
|
*/
|
||||||
|
fun applyPlaylistSort(sort: Sort) {
|
||||||
|
listSettings.playlistSort = sort
|
||||||
|
_playlistInstructions.put(UpdateInstructions.Replace(0))
|
||||||
|
_playlistList.value = listSettings.playlistSort.playlists(_playlistList.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update [currentTabType] to reflect a new ViewPager2 position
|
||||||
*
|
*
|
||||||
* @param pagerPos The new position of the ViewPager2 instance.
|
* @param pagerPos The new position of the ViewPager2 instance.
|
||||||
*/
|
*/
|
||||||
fun synchronizeTabPosition(pagerPos: Int) {
|
fun synchronizeTabPosition(pagerPos: Int) {
|
||||||
logD("Updating current tab to ${currentTabModes[pagerPos]}")
|
logD("Updating current tab to ${currentTabTypes[pagerPos]}")
|
||||||
_currentTabMode.value = currentTabModes[pagerPos]
|
_currentTabType.value = currentTabTypes[pagerPos]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -264,12 +293,26 @@ constructor(
|
||||||
_isFastScrolling.value = isFastScrolling
|
_isFastScrolling.value = isFastScrolling
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showSettings() {
|
||||||
|
_showOuter.put(Outer.Settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showAbout() {
|
||||||
|
_showOuter.put(Outer.About)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a list of [MusicMode]s representing a simpler version of the [Tab] configuration.
|
* Create a list of [MusicType]s representing a simpler version of the [Tab] configuration.
|
||||||
*
|
*
|
||||||
* @return A list of the [MusicMode]s for each visible [Tab] in the configuration, ordered in
|
* @return A list of the [MusicType]s for each visible [Tab] in the configuration, ordered in
|
||||||
* the same way as the configuration.
|
* the same way as the configuration.
|
||||||
*/
|
*/
|
||||||
private fun makeTabModes() =
|
private fun makeTabTypes() =
|
||||||
homeSettings.homeTabs.filterIsInstance<Tab.Visible>().map { it.mode }
|
homeSettings.homeTabs.filterIsInstance<Tab.Visible>().map { it.type }
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface Outer {
|
||||||
|
data object Settings : Outer
|
||||||
|
|
||||||
|
data object About : Outer
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,8 +123,11 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isAutoMirrored(): Boolean = true
|
override fun isAutoMirrored(): Boolean = true
|
||||||
|
|
||||||
override fun setAlpha(alpha: Int) {}
|
override fun setAlpha(alpha: Int) {}
|
||||||
|
|
||||||
override fun setColorFilter(colorFilter: ColorFilter?) {}
|
override fun setColorFilter(colorFilter: ColorFilter?) {}
|
||||||
|
|
||||||
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
|
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
|
||||||
|
|
||||||
private fun updatePath() {
|
private fun updatePath() {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ package org.oxycblt.auxio.home.list
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
@ -34,12 +33,11 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
import org.oxycblt.auxio.list.ListViewModel
|
import org.oxycblt.auxio.list.ListViewModel
|
||||||
import org.oxycblt.auxio.list.SelectableListListener
|
import org.oxycblt.auxio.list.SelectableListListener
|
||||||
import org.oxycblt.auxio.list.Sort
|
|
||||||
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.AlbumViewHolder
|
import org.oxycblt.auxio.list.recycler.AlbumViewHolder
|
||||||
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
|
@ -81,7 +79,7 @@ class AlbumListFragment :
|
||||||
listener = this@AlbumListFragment
|
listener = this@AlbumListFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
collectImmediately(homeModel.albumsList, ::updateAlbums)
|
collectImmediately(homeModel.albumList, ::updateAlbums)
|
||||||
collectImmediately(listModel.selected, ::updateSelection)
|
collectImmediately(listModel.selected, ::updateSelection)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
|
|
@ -97,9 +95,9 @@ class AlbumListFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPopup(pos: Int): String? {
|
override fun getPopup(pos: Int): String? {
|
||||||
val album = homeModel.albumsList.value[pos]
|
val album = homeModel.albumList.value[pos]
|
||||||
// Change how we display the popup depending on the current sort mode.
|
// Change how we display the popup depending on the current sort mode.
|
||||||
return when (homeModel.getSortForTab(MusicMode.ALBUMS).mode) {
|
return when (homeModel.albumSort.mode) {
|
||||||
// By Name -> Use Name
|
// By Name -> Use Name
|
||||||
is Sort.Mode.ByName -> album.name.thumb
|
is Sort.Mode.ByName -> album.name.thumb
|
||||||
|
|
||||||
|
|
@ -141,12 +139,12 @@ class AlbumListFragment :
|
||||||
detailModel.showAlbum(item)
|
detailModel.showAlbum(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Album, anchor: View) {
|
override fun onOpenMenu(item: Album) {
|
||||||
listModel.openMenu(R.menu.item_album, item)
|
listModel.openMenu(R.menu.item_album, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateAlbums(albums: List<Album>) {
|
private fun updateAlbums(albums: List<Album>) {
|
||||||
albumAdapter.update(albums, homeModel.albumsInstructions.consume())
|
albumAdapter.update(albums, homeModel.albumInstructions.consume())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selection: List<Music>) {
|
private fun updateSelection(selection: List<Music>) {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
@ -32,12 +31,11 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
import org.oxycblt.auxio.list.ListViewModel
|
import org.oxycblt.auxio.list.ListViewModel
|
||||||
import org.oxycblt.auxio.list.SelectableListListener
|
import org.oxycblt.auxio.list.SelectableListListener
|
||||||
import org.oxycblt.auxio.list.Sort
|
|
||||||
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.ArtistViewHolder
|
import org.oxycblt.auxio.list.recycler.ArtistViewHolder
|
||||||
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
|
@ -76,7 +74,7 @@ class ArtistListFragment :
|
||||||
listener = this@ArtistListFragment
|
listener = this@ArtistListFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
collectImmediately(homeModel.artistsList, ::updateArtists)
|
collectImmediately(homeModel.artistList, ::updateArtists)
|
||||||
collectImmediately(listModel.selected, ::updateSelection)
|
collectImmediately(listModel.selected, ::updateSelection)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
|
|
@ -92,9 +90,9 @@ class ArtistListFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPopup(pos: Int): String? {
|
override fun getPopup(pos: Int): String? {
|
||||||
val artist = homeModel.artistsList.value[pos]
|
val artist = homeModel.artistList.value[pos]
|
||||||
// Change how we display the popup depending on the current sort mode.
|
// Change how we display the popup depending on the current sort mode.
|
||||||
return when (homeModel.getSortForTab(MusicMode.ARTISTS).mode) {
|
return when (homeModel.artistSort.mode) {
|
||||||
// By Name -> Use Name
|
// By Name -> Use Name
|
||||||
is Sort.Mode.ByName -> artist.name.thumb
|
is Sort.Mode.ByName -> artist.name.thumb
|
||||||
|
|
||||||
|
|
@ -117,12 +115,12 @@ class ArtistListFragment :
|
||||||
detailModel.showArtist(item)
|
detailModel.showArtist(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Artist, anchor: View) {
|
override fun onOpenMenu(item: Artist) {
|
||||||
listModel.openMenu(R.menu.item_parent, item)
|
listModel.openMenu(R.menu.item_parent, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateArtists(artists: List<Artist>) {
|
private fun updateArtists(artists: List<Artist>) {
|
||||||
artistAdapter.update(artists, homeModel.artistsInstructions.consume())
|
artistAdapter.update(artists, homeModel.artistInstructions.consume())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selection: List<Music>) {
|
private fun updateSelection(selection: List<Music>) {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
@ -32,12 +31,11 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
import org.oxycblt.auxio.list.ListViewModel
|
import org.oxycblt.auxio.list.ListViewModel
|
||||||
import org.oxycblt.auxio.list.SelectableListListener
|
import org.oxycblt.auxio.list.SelectableListListener
|
||||||
import org.oxycblt.auxio.list.Sort
|
|
||||||
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.GenreViewHolder
|
import org.oxycblt.auxio.list.recycler.GenreViewHolder
|
||||||
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
|
@ -75,7 +73,7 @@ class GenreListFragment :
|
||||||
listener = this@GenreListFragment
|
listener = this@GenreListFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
collectImmediately(homeModel.genresList, ::updateGenres)
|
collectImmediately(homeModel.genreList, ::updateGenres)
|
||||||
collectImmediately(listModel.selected, ::updateSelection)
|
collectImmediately(listModel.selected, ::updateSelection)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
|
|
@ -91,9 +89,9 @@ class GenreListFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPopup(pos: Int): String? {
|
override fun getPopup(pos: Int): String? {
|
||||||
val genre = homeModel.genresList.value[pos]
|
val genre = homeModel.genreList.value[pos]
|
||||||
// Change how we display the popup depending on the current sort mode.
|
// Change how we display the popup depending on the current sort mode.
|
||||||
return when (homeModel.getSortForTab(MusicMode.GENRES).mode) {
|
return when (homeModel.genreSort.mode) {
|
||||||
// By Name -> Use Name
|
// By Name -> Use Name
|
||||||
is Sort.Mode.ByName -> genre.name.thumb
|
is Sort.Mode.ByName -> genre.name.thumb
|
||||||
|
|
||||||
|
|
@ -116,12 +114,12 @@ class GenreListFragment :
|
||||||
detailModel.showGenre(item)
|
detailModel.showGenre(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Genre, anchor: View) {
|
override fun onOpenMenu(item: Genre) {
|
||||||
listModel.openMenu(R.menu.item_parent, item)
|
listModel.openMenu(R.menu.item_parent, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateGenres(genres: List<Genre>) {
|
private fun updateGenres(genres: List<Genre>) {
|
||||||
genreAdapter.update(genres, homeModel.genresInstructions.consume())
|
genreAdapter.update(genres, homeModel.genreInstructions.consume())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selection: List<Music>) {
|
private fun updateSelection(selection: List<Music>) {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
|
@ -31,11 +30,10 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
import org.oxycblt.auxio.list.ListViewModel
|
import org.oxycblt.auxio.list.ListViewModel
|
||||||
import org.oxycblt.auxio.list.SelectableListListener
|
import org.oxycblt.auxio.list.SelectableListListener
|
||||||
import org.oxycblt.auxio.list.Sort
|
|
||||||
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.PlaylistViewHolder
|
import org.oxycblt.auxio.list.recycler.PlaylistViewHolder
|
||||||
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
|
|
@ -73,7 +71,7 @@ class PlaylistListFragment :
|
||||||
listener = this@PlaylistListFragment
|
listener = this@PlaylistListFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
collectImmediately(homeModel.playlistsList, ::updatePlaylists)
|
collectImmediately(homeModel.playlistList, ::updatePlaylists)
|
||||||
collectImmediately(listModel.selected, ::updateSelection)
|
collectImmediately(listModel.selected, ::updateSelection)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
|
|
@ -89,9 +87,9 @@ class PlaylistListFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPopup(pos: Int): String? {
|
override fun getPopup(pos: Int): String? {
|
||||||
val playlist = homeModel.playlistsList.value[pos]
|
val playlist = homeModel.playlistList.value[pos]
|
||||||
// Change how we display the popup depending on the current sort mode.
|
// Change how we display the popup depending on the current sort mode.
|
||||||
return when (homeModel.getSortForTab(MusicMode.GENRES).mode) {
|
return when (homeModel.playlistSort.mode) {
|
||||||
// By Name -> Use Name
|
// By Name -> Use Name
|
||||||
is Sort.Mode.ByName -> playlist.name.thumb
|
is Sort.Mode.ByName -> playlist.name.thumb
|
||||||
|
|
||||||
|
|
@ -114,12 +112,12 @@ class PlaylistListFragment :
|
||||||
detailModel.showPlaylist(item)
|
detailModel.showPlaylist(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Playlist, anchor: View) {
|
override fun onOpenMenu(item: Playlist) {
|
||||||
listModel.openMenu(R.menu.item_playlist, item)
|
listModel.openMenu(R.menu.item_playlist, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePlaylists(playlists: List<Playlist>) {
|
private fun updatePlaylists(playlists: List<Playlist>) {
|
||||||
playlistAdapter.update(playlists, homeModel.playlistsInstructions.consume())
|
playlistAdapter.update(playlists, homeModel.playlistInstructions.consume())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selection: List<Music>) {
|
private fun updateSelection(selection: List<Music>) {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ package org.oxycblt.auxio.home.list
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
@ -33,11 +32,10 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
import org.oxycblt.auxio.list.ListViewModel
|
import org.oxycblt.auxio.list.ListViewModel
|
||||||
import org.oxycblt.auxio.list.SelectableListListener
|
import org.oxycblt.auxio.list.SelectableListListener
|
||||||
import org.oxycblt.auxio.list.Sort
|
|
||||||
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
||||||
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
|
@ -78,7 +76,7 @@ class SongListFragment :
|
||||||
listener = this@SongListFragment
|
listener = this@SongListFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
collectImmediately(homeModel.songsList, ::updateSongs)
|
collectImmediately(homeModel.songList, ::updateSongs)
|
||||||
collectImmediately(listModel.selected, ::updateSelection)
|
collectImmediately(listModel.selected, ::updateSelection)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
|
|
@ -94,11 +92,11 @@ class SongListFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPopup(pos: Int): String? {
|
override fun getPopup(pos: Int): String? {
|
||||||
val song = homeModel.songsList.value[pos]
|
val song = homeModel.songList.value[pos]
|
||||||
// Change how we display the popup depending on the current sort mode.
|
// Change how we display the popup depending on the current sort mode.
|
||||||
// Note: We don't use the more correct individual artist name here, as sorts are largely
|
// Note: We don't use the more correct individual artist name here, as sorts are largely
|
||||||
// based off the names of the parent objects and not the child objects.
|
// based off the names of the parent objects and not the child objects.
|
||||||
return when (homeModel.getSortForTab(MusicMode.SONGS).mode) {
|
return when (homeModel.songSort.mode) {
|
||||||
// Name -> Use name
|
// Name -> Use name
|
||||||
is Sort.Mode.ByName -> song.name.thumb
|
is Sort.Mode.ByName -> song.name.thumb
|
||||||
|
|
||||||
|
|
@ -137,15 +135,15 @@ class SongListFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRealClick(item: Song) {
|
override fun onRealClick(item: Song) {
|
||||||
playbackModel.playFrom(item, homeModel.playbackMode)
|
playbackModel.play(item, homeModel.playWith)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Song, anchor: View) {
|
override fun onOpenMenu(item: Song) {
|
||||||
listModel.openMenu(R.menu.item_song, item)
|
listModel.openMenu(R.menu.item_song, item, homeModel.playWith)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSongs(songs: List<Song>) {
|
private fun updateSongs(songs: List<Song>) {
|
||||||
songAdapter.update(songs, homeModel.songsInstructions.consume())
|
songAdapter.update(songs, homeModel.songInstructions.consume())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selection: List<Music>) {
|
private fun updateSelection(selection: List<Music>) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* AlbumSortDialog.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.home.sort
|
||||||
|
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.oxycblt.auxio.home.HomeViewModel
|
||||||
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
|
import org.oxycblt.auxio.list.sort.SortDialog
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [SortDialog] that controls the [Sort] of [HomeViewModel.albumList].
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class AlbumSortDialog : SortDialog() {
|
||||||
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun getInitialSort() = homeModel.albumSort
|
||||||
|
|
||||||
|
override fun applyChosenSort(sort: Sort) {
|
||||||
|
homeModel.applyAlbumSort(sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getModeChoices() =
|
||||||
|
listOf(
|
||||||
|
Sort.Mode.ByName,
|
||||||
|
Sort.Mode.ByArtist,
|
||||||
|
Sort.Mode.ByDate,
|
||||||
|
Sort.Mode.ByDuration,
|
||||||
|
Sort.Mode.ByCount,
|
||||||
|
Sort.Mode.ByDateAdded)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* ArtistSortDialog.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.home.sort
|
||||||
|
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.oxycblt.auxio.home.HomeViewModel
|
||||||
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
|
import org.oxycblt.auxio.list.sort.SortDialog
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [SortDialog] that controls the [Sort] of [HomeViewModel.artistList].
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class ArtistSortDialog : SortDialog() {
|
||||||
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun getInitialSort() = homeModel.artistSort
|
||||||
|
|
||||||
|
override fun applyChosenSort(sort: Sort) {
|
||||||
|
homeModel.applyArtistSort(sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getModeChoices() =
|
||||||
|
listOf(Sort.Mode.ByName, Sort.Mode.ByDuration, Sort.Mode.ByCount)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* GenreSortDialog.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.home.sort
|
||||||
|
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.oxycblt.auxio.home.HomeViewModel
|
||||||
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
|
import org.oxycblt.auxio.list.sort.SortDialog
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [SortDialog] that controls the [Sort] of [HomeViewModel.genreList].
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class GenreSortDialog : SortDialog() {
|
||||||
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun getInitialSort() = homeModel.genreSort
|
||||||
|
|
||||||
|
override fun applyChosenSort(sort: Sort) {
|
||||||
|
homeModel.applyGenreSort(sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getModeChoices() =
|
||||||
|
listOf(Sort.Mode.ByName, Sort.Mode.ByDuration, Sort.Mode.ByCount)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* PlaylistSortDialog.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.home.sort
|
||||||
|
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.oxycblt.auxio.home.HomeViewModel
|
||||||
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
|
import org.oxycblt.auxio.list.sort.SortDialog
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [SortDialog] that controls the [Sort] of [HomeViewModel.playlistList].
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class PlaylistSortDialog : SortDialog() {
|
||||||
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun getInitialSort() = homeModel.playlistSort
|
||||||
|
|
||||||
|
override fun applyChosenSort(sort: Sort) {
|
||||||
|
homeModel.applyPlaylistSort(sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getModeChoices() =
|
||||||
|
listOf(Sort.Mode.ByName, Sort.Mode.ByDuration, Sort.Mode.ByCount)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* SongSortDialog.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.home.sort
|
||||||
|
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.oxycblt.auxio.home.HomeViewModel
|
||||||
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
|
import org.oxycblt.auxio.list.sort.SortDialog
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [SortDialog] that controls the [Sort] of [HomeViewModel.songList].
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class SongSortDialog : SortDialog() {
|
||||||
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun getInitialSort() = homeModel.songSort
|
||||||
|
|
||||||
|
override fun applyChosenSort(sort: Sort) {
|
||||||
|
homeModel.applySongSort(sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getModeChoices() =
|
||||||
|
listOf(
|
||||||
|
Sort.Mode.ByName,
|
||||||
|
Sort.Mode.ByArtist,
|
||||||
|
Sort.Mode.ByAlbum,
|
||||||
|
Sort.Mode.ByDate,
|
||||||
|
Sort.Mode.ByDuration,
|
||||||
|
Sort.Mode.ByDateAdded)
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,7 @@ import android.content.Context
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicType
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [TabLayoutMediator.TabConfigurationStrategy] that uses larger/smaller tab configurations
|
* A [TabLayoutMediator.TabConfigurationStrategy] that uses larger/smaller tab configurations
|
||||||
|
|
@ -32,7 +32,7 @@ import org.oxycblt.auxio.music.MusicMode
|
||||||
* @param tabs Current tab configuration from settings
|
* @param tabs Current tab configuration from settings
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class AdaptiveTabStrategy(context: Context, private val tabs: List<MusicMode>) :
|
class AdaptiveTabStrategy(context: Context, private val tabs: List<MusicType>) :
|
||||||
TabLayoutMediator.TabConfigurationStrategy {
|
TabLayoutMediator.TabConfigurationStrategy {
|
||||||
private val width = context.resources.configuration.smallestScreenWidthDp
|
private val width = context.resources.configuration.smallestScreenWidthDp
|
||||||
|
|
||||||
|
|
@ -41,23 +41,23 @@ class AdaptiveTabStrategy(context: Context, private val tabs: List<MusicMode>) :
|
||||||
val string: Int
|
val string: Int
|
||||||
|
|
||||||
when (tabs[position]) {
|
when (tabs[position]) {
|
||||||
MusicMode.SONGS -> {
|
MusicType.SONGS -> {
|
||||||
icon = R.drawable.ic_song_24
|
icon = R.drawable.ic_song_24
|
||||||
string = R.string.lbl_songs
|
string = R.string.lbl_songs
|
||||||
}
|
}
|
||||||
MusicMode.ALBUMS -> {
|
MusicType.ALBUMS -> {
|
||||||
icon = R.drawable.ic_album_24
|
icon = R.drawable.ic_album_24
|
||||||
string = R.string.lbl_albums
|
string = R.string.lbl_albums
|
||||||
}
|
}
|
||||||
MusicMode.ARTISTS -> {
|
MusicType.ARTISTS -> {
|
||||||
icon = R.drawable.ic_artist_24
|
icon = R.drawable.ic_artist_24
|
||||||
string = R.string.lbl_artists
|
string = R.string.lbl_artists
|
||||||
}
|
}
|
||||||
MusicMode.GENRES -> {
|
MusicType.GENRES -> {
|
||||||
icon = R.drawable.ic_genre_24
|
icon = R.drawable.ic_genre_24
|
||||||
string = R.string.lbl_genres
|
string = R.string.lbl_genres
|
||||||
}
|
}
|
||||||
MusicMode.PLAYLISTS -> {
|
MusicType.PLAYLISTS -> {
|
||||||
icon = R.drawable.ic_playlist_24
|
icon = R.drawable.ic_playlist_24
|
||||||
string = R.string.lbl_playlists
|
string = R.string.lbl_playlists
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,30 +18,30 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.tabs
|
package org.oxycblt.auxio.home.tabs
|
||||||
|
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicType
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A representation of a library tab suitable for configuration.
|
* A representation of a library tab suitable for configuration.
|
||||||
*
|
*
|
||||||
* @param mode The type of list in the home view this instance corresponds to.
|
* @param type 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 type: MusicType) {
|
||||||
/**
|
/**
|
||||||
* 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 type The type of list in the home view this instance corresponds to.
|
||||||
*/
|
*/
|
||||||
data class Visible(override val mode: MusicMode) : Tab(mode)
|
data class Visible(override val type: MusicType) : Tab(type)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A visible tab. This will be visible in the tab configuration view, but not in the home view.
|
* A visible tab. This will be visible in the tab configuration view, but not in the home view.
|
||||||
*
|
*
|
||||||
* @param mode The type of list in the home view this instance corresponds to.
|
* @param type The type of list in the home view this instance corresponds to.
|
||||||
*/
|
*/
|
||||||
data class Invisible(override val mode: MusicMode) : Tab(mode)
|
data class Invisible(override val type: MusicType) : Tab(type)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// Like other IO-bound datatypes in Auxio, tabs are stored in a binary format. However, tabs
|
// Like other IO-bound datatypes in Auxio, tabs are stored in a binary format. However, tabs
|
||||||
|
|
@ -67,14 +67,14 @@ sealed class Tab(open val mode: MusicMode) {
|
||||||
*/
|
*/
|
||||||
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_1100
|
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_1100
|
||||||
|
|
||||||
/** Maps between the integer code in the tab sequence and it's [MusicMode]. */
|
/** Maps between the integer code in the tab sequence and it's [MusicType]. */
|
||||||
private val MODE_TABLE =
|
private val MODE_TABLE =
|
||||||
arrayOf(
|
arrayOf(
|
||||||
MusicMode.SONGS,
|
MusicType.SONGS,
|
||||||
MusicMode.ALBUMS,
|
MusicType.ALBUMS,
|
||||||
MusicMode.ARTISTS,
|
MusicType.ARTISTS,
|
||||||
MusicMode.GENRES,
|
MusicType.GENRES,
|
||||||
MusicMode.PLAYLISTS)
|
MusicType.PLAYLISTS)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert an array of [Tab]s into it's integer representation.
|
* Convert an array of [Tab]s into it's integer representation.
|
||||||
|
|
@ -84,7 +84,7 @@ sealed class Tab(open val mode: MusicMode) {
|
||||||
*/
|
*/
|
||||||
fun toIntCode(tabs: Array<Tab>): Int {
|
fun toIntCode(tabs: Array<Tab>): Int {
|
||||||
// Like when deserializing, make sure there are no duplicate tabs for whatever reason.
|
// Like when deserializing, make sure there are no duplicate tabs for whatever reason.
|
||||||
val distinct = tabs.distinctBy { it.mode }
|
val distinct = tabs.distinctBy { it.type }
|
||||||
if (tabs.size != distinct.size) {
|
if (tabs.size != distinct.size) {
|
||||||
logW(
|
logW(
|
||||||
"Tab sequences should not have duplicates [old: ${tabs.size} new: ${distinct.size}]")
|
"Tab sequences should not have duplicates [old: ${tabs.size} new: ${distinct.size}]")
|
||||||
|
|
@ -95,8 +95,8 @@ sealed class Tab(open val mode: MusicMode) {
|
||||||
for (tab in distinct) {
|
for (tab in distinct) {
|
||||||
val bin =
|
val bin =
|
||||||
when (tab) {
|
when (tab) {
|
||||||
is Visible -> 1.shl(3) or MODE_TABLE.indexOf(tab.mode)
|
is Visible -> 1.shl(3) or MODE_TABLE.indexOf(tab.type)
|
||||||
is Invisible -> MODE_TABLE.indexOf(tab.mode)
|
is Invisible -> MODE_TABLE.indexOf(tab.type)
|
||||||
}
|
}
|
||||||
|
|
||||||
sequence = sequence or bin.shl(shift)
|
sequence = sequence or bin.shl(shift)
|
||||||
|
|
@ -131,7 +131,7 @@ sealed class Tab(open val mode: MusicMode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure there are no duplicate tabs
|
// Make sure there are no duplicate tabs
|
||||||
val distinct = tabs.distinctBy { it.mode }
|
val distinct = tabs.distinctBy { it.type }
|
||||||
if (tabs.size != distinct.size) {
|
if (tabs.size != distinct.size) {
|
||||||
logW(
|
logW(
|
||||||
"Tab sequences should not have duplicates [old: ${tabs.size} new: ${distinct.size}]")
|
"Tab sequences should not have duplicates [old: ${tabs.size} new: ${distinct.size}]")
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.ItemTabBinding
|
import org.oxycblt.auxio.databinding.ItemTabBinding
|
||||||
import org.oxycblt.auxio.list.EditClickListListener
|
import org.oxycblt.auxio.list.EditClickListListener
|
||||||
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.MusicType
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
|
@ -42,7 +42,9 @@ class TabAdapter(private val listener: EditClickListListener<Tab>) :
|
||||||
private set
|
private set
|
||||||
|
|
||||||
override fun getItemCount() = tabs.size
|
override fun getItemCount() = tabs.size
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = TabViewHolder.from(parent)
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = TabViewHolder.from(parent)
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
|
||||||
holder.bind(tabs[position], listener)
|
holder.bind(tabs[position], listener)
|
||||||
}
|
}
|
||||||
|
|
@ -107,14 +109,14 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) :
|
||||||
fun bind(tab: Tab, listener: EditClickListListener<Tab>) {
|
fun bind(tab: Tab, listener: EditClickListListener<Tab>) {
|
||||||
listener.bind(tab, this, dragHandle = binding.tabDragHandle)
|
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 type
|
||||||
setText(
|
setText(
|
||||||
when (tab.mode) {
|
when (tab.type) {
|
||||||
MusicMode.SONGS -> R.string.lbl_songs
|
MusicType.SONGS -> R.string.lbl_songs
|
||||||
MusicMode.ALBUMS -> R.string.lbl_albums
|
MusicType.ALBUMS -> R.string.lbl_albums
|
||||||
MusicMode.ARTISTS -> R.string.lbl_artists
|
MusicType.ARTISTS -> R.string.lbl_artists
|
||||||
MusicMode.GENRES -> R.string.lbl_genres
|
MusicType.GENRES -> R.string.lbl_genres
|
||||||
MusicMode.PLAYLISTS -> R.string.lbl_playlists
|
MusicType.PLAYLISTS -> R.string.lbl_playlists
|
||||||
})
|
})
|
||||||
|
|
||||||
// Unlike in other adapters, we update the checked state alongside
|
// Unlike in other adapters, we update the checked state alongside
|
||||||
|
|
|
||||||
|
|
@ -91,13 +91,13 @@ class TabCustomizeDialog :
|
||||||
override fun onClick(item: Tab, viewHolder: RecyclerView.ViewHolder) {
|
override fun onClick(item: Tab, viewHolder: RecyclerView.ViewHolder) {
|
||||||
// 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 == item.mode }
|
val index = tabAdapter.tabs.indexOfFirst { it.type == item.type }
|
||||||
val old = tabAdapter.tabs[index]
|
val old = tabAdapter.tabs[index]
|
||||||
val new =
|
val new =
|
||||||
when (old) {
|
when (old) {
|
||||||
// Invert the visibility of the tab
|
// Invert the visibility of the tab
|
||||||
is Tab.Visible -> Tab.Invisible(old.mode)
|
is Tab.Visible -> Tab.Invisible(old.type)
|
||||||
is Tab.Invisible -> Tab.Visible(old.mode)
|
is Tab.Invisible -> Tab.Visible(old.type)
|
||||||
}
|
}
|
||||||
logD("Flipping tab visibility [from: $old to: $new]")
|
logD("Flipping tab visibility [from: $old to: $new]")
|
||||||
tabAdapter.setTab(index, new)
|
tabAdapter.setTab(index, new)
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ import org.oxycblt.auxio.util.getInteger
|
||||||
class CoverView
|
class CoverView
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||||
FrameLayout(context, attrs, defStyleAttr), ImageSettings.Listener, UISettings.Listener {
|
FrameLayout(context, attrs, defStyleAttr) {
|
||||||
@Inject lateinit var imageLoader: ImageLoader
|
@Inject lateinit var imageLoader: ImageLoader
|
||||||
@Inject lateinit var uiSettings: UISettings
|
@Inject lateinit var uiSettings: UISettings
|
||||||
@Inject lateinit var imageSettings: ImageSettings
|
@Inject lateinit var imageSettings: ImageSettings
|
||||||
|
|
@ -88,6 +88,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
val playingDrawable: AnimationDrawable,
|
val playingDrawable: AnimationDrawable,
|
||||||
val pausedDrawable: Drawable
|
val pausedDrawable: Drawable
|
||||||
)
|
)
|
||||||
|
|
||||||
private val playbackIndicator: PlaybackIndicator?
|
private val playbackIndicator: PlaybackIndicator?
|
||||||
private val selectionBadge: ImageView?
|
private val selectionBadge: ImageView?
|
||||||
|
|
||||||
|
|
@ -105,6 +106,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
val desc: String,
|
val desc: String,
|
||||||
@DrawableRes val errorRes: Int
|
@DrawableRes val errorRes: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
private var currentCover: Cover? = null
|
private var currentCover: Cover? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
@ -152,9 +154,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
imageSettings.registerListener(this)
|
|
||||||
uiSettings.registerListener(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFinishInflate() {
|
override fun onFinishInflate() {
|
||||||
|
|
@ -185,24 +184,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetachedFromWindow() {
|
|
||||||
super.onDetachedFromWindow()
|
|
||||||
imageSettings.unregisterListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onImageSettingsChanged() {
|
|
||||||
val cover = currentCover ?: return
|
|
||||||
bind(cover.songs, cover.desc, cover.errorRes)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRoundModeChanged() {
|
|
||||||
// TODO: Make this a recreate as soon as you can make the bottom sheet stop freaking out
|
|
||||||
cornerRadiusRes = getCornerRadiusRes()
|
|
||||||
applyBackgroundsToChildren()
|
|
||||||
val cover = currentCover ?: return
|
|
||||||
bind(cover.songs, cover.desc, cover.errorRes)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||||
|
|
||||||
|
|
@ -274,7 +255,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCornerRadiusRes() =
|
private fun getCornerRadiusRes() =
|
||||||
if (uiSettings.roundMode) {
|
if (!isInEditMode && uiSettings.roundMode) {
|
||||||
SIZING_CORNER_RADII[sizing]
|
SIZING_CORNER_RADII[sizing]
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ import okio.buffer
|
||||||
import okio.source
|
import okio.source
|
||||||
import org.oxycblt.auxio.image.CoverMode
|
import org.oxycblt.auxio.image.CoverMode
|
||||||
import org.oxycblt.auxio.image.ImageSettings
|
import org.oxycblt.auxio.image.ImageSettings
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,9 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.list
|
package org.oxycblt.auxio.list
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.annotation.MenuRes
|
|
||||||
import androidx.appcompat.widget.PopupMenu
|
|
||||||
import androidx.core.view.MenuCompat
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Fragment containing a selectable list.
|
* A Fragment containing a selectable list.
|
||||||
|
|
@ -34,14 +29,6 @@ import org.oxycblt.auxio.util.logD
|
||||||
*/
|
*/
|
||||||
abstract class ListFragment<in T : Music, VB : ViewBinding> :
|
abstract class ListFragment<in T : Music, VB : ViewBinding> :
|
||||||
SelectionFragment<VB>(), SelectableListListener<T> {
|
SelectionFragment<VB>(), SelectableListListener<T> {
|
||||||
private var currentMenu: PopupMenu? = null
|
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: VB) {
|
|
||||||
super.onDestroyBinding(binding)
|
|
||||||
currentMenu?.dismiss()
|
|
||||||
currentMenu = null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when [onClick] is called, but does not result in the item being selected. This more or
|
* Called when [onClick] is called, but does not result in the item being selected. This more or
|
||||||
* less corresponds to an [onClick] implementation in a non-[ListFragment].
|
* less corresponds to an [onClick] implementation in a non-[ListFragment].
|
||||||
|
|
@ -63,30 +50,4 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
|
||||||
final override fun onSelect(item: T) {
|
final override fun onSelect(item: T) {
|
||||||
listModel.select(item)
|
listModel.select(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Open a menu. This menu will be managed by the Fragment and closed when the view is destroyed.
|
|
||||||
* If a menu is already opened, this call is ignored.
|
|
||||||
*
|
|
||||||
* @param anchor The [View] to anchor the menu to.
|
|
||||||
* @param menuRes The resource of the menu to load.
|
|
||||||
* @param block A block that is ran within [PopupMenu] that allows further configuration.
|
|
||||||
*/
|
|
||||||
protected fun openMenu(anchor: View, @MenuRes menuRes: Int, block: PopupMenu.() -> Unit) {
|
|
||||||
if (currentMenu != null) {
|
|
||||||
logD("Menu already present, not launching")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logD("Opening popup menu menu")
|
|
||||||
|
|
||||||
currentMenu =
|
|
||||||
PopupMenu(requireContext(), anchor).apply {
|
|
||||||
inflate(menuRes)
|
|
||||||
MenuCompat.setGroupDividerEnabled(menu, true)
|
|
||||||
block()
|
|
||||||
setOnDismissListener { currentMenu = null }
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
30
app/src/main/java/org/oxycblt/auxio/list/ListModule.kt
Normal file
30
app/src/main/java/org/oxycblt/auxio/list/ListModule.kt
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* ListModule.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.list
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
interface ListModule {
|
||||||
|
@Binds fun settings(settings: ListSettingsImpl): ListSettings
|
||||||
|
}
|
||||||
148
app/src/main/java/org/oxycblt/auxio/list/ListSettings.kt
Normal file
148
app/src/main/java/org/oxycblt/auxio/list/ListSettings.kt
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* ListSettings.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.list
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
|
import org.oxycblt.auxio.settings.Settings
|
||||||
|
|
||||||
|
interface ListSettings : Settings<Unit> {
|
||||||
|
/** The [Sort] mode used in Song lists. */
|
||||||
|
var songSort: Sort
|
||||||
|
/** The [Sort] mode used in Album lists. */
|
||||||
|
var albumSort: Sort
|
||||||
|
/** The [Sort] mode used in Artist lists. */
|
||||||
|
var artistSort: Sort
|
||||||
|
/** The [Sort] mode used in Genre lists. */
|
||||||
|
var genreSort: Sort
|
||||||
|
/** The [Sort] mode used in Playlist lists. */
|
||||||
|
var playlistSort: Sort
|
||||||
|
/** The [Sort] mode used in an Album's Song list. */
|
||||||
|
var albumSongSort: Sort
|
||||||
|
/** The [Sort] mode used in an Artist's Song list. */
|
||||||
|
var artistSongSort: Sort
|
||||||
|
/** The [Sort] mode used in a Genre's Song list. */
|
||||||
|
var genreSongSort: Sort
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListSettingsImpl @Inject constructor(@ApplicationContext val context: Context) :
|
||||||
|
Settings.Impl<Unit>(context), ListSettings {
|
||||||
|
override var songSort: Sort
|
||||||
|
get() =
|
||||||
|
Sort.fromIntCode(
|
||||||
|
sharedPreferences.getInt(getString(R.string.set_key_songs_sort), Int.MIN_VALUE))
|
||||||
|
?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
|
||||||
|
set(value) {
|
||||||
|
sharedPreferences.edit {
|
||||||
|
putInt(getString(R.string.set_key_songs_sort), value.intCode)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var albumSort: Sort
|
||||||
|
get() =
|
||||||
|
Sort.fromIntCode(
|
||||||
|
sharedPreferences.getInt(getString(R.string.set_key_albums_sort), Int.MIN_VALUE))
|
||||||
|
?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
|
||||||
|
set(value) {
|
||||||
|
sharedPreferences.edit {
|
||||||
|
putInt(getString(R.string.set_key_albums_sort), value.intCode)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var artistSort: Sort
|
||||||
|
get() =
|
||||||
|
Sort.fromIntCode(
|
||||||
|
sharedPreferences.getInt(getString(R.string.set_key_artists_sort), Int.MIN_VALUE))
|
||||||
|
?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
|
||||||
|
set(value) {
|
||||||
|
sharedPreferences.edit {
|
||||||
|
putInt(getString(R.string.set_key_artists_sort), value.intCode)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var genreSort: Sort
|
||||||
|
get() =
|
||||||
|
Sort.fromIntCode(
|
||||||
|
sharedPreferences.getInt(getString(R.string.set_key_genres_sort), Int.MIN_VALUE))
|
||||||
|
?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
|
||||||
|
set(value) {
|
||||||
|
sharedPreferences.edit {
|
||||||
|
putInt(getString(R.string.set_key_genres_sort), value.intCode)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var playlistSort: Sort
|
||||||
|
get() =
|
||||||
|
Sort.fromIntCode(
|
||||||
|
sharedPreferences.getInt(getString(R.string.set_key_playlists_sort), Int.MIN_VALUE))
|
||||||
|
?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
|
||||||
|
set(value) {
|
||||||
|
sharedPreferences.edit {
|
||||||
|
putInt(getString(R.string.set_key_playlists_sort), value.intCode)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var albumSongSort: Sort
|
||||||
|
get() =
|
||||||
|
Sort.fromIntCode(
|
||||||
|
sharedPreferences.getInt(
|
||||||
|
getString(R.string.set_key_album_songs_sort), Int.MIN_VALUE))
|
||||||
|
?: Sort(Sort.Mode.ByDisc, Sort.Direction.ASCENDING)
|
||||||
|
set(value) {
|
||||||
|
sharedPreferences.edit {
|
||||||
|
putInt(getString(R.string.set_key_album_songs_sort), value.intCode)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var artistSongSort: Sort
|
||||||
|
get() =
|
||||||
|
Sort.fromIntCode(
|
||||||
|
sharedPreferences.getInt(
|
||||||
|
getString(R.string.set_key_artist_songs_sort), Int.MIN_VALUE))
|
||||||
|
?: Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING)
|
||||||
|
set(value) {
|
||||||
|
sharedPreferences.edit {
|
||||||
|
putInt(getString(R.string.set_key_artist_songs_sort), value.intCode)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var genreSongSort: Sort
|
||||||
|
get() =
|
||||||
|
Sort.fromIntCode(
|
||||||
|
sharedPreferences.getInt(
|
||||||
|
getString(R.string.set_key_genre_songs_sort), Int.MIN_VALUE))
|
||||||
|
?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
|
||||||
|
set(value) {
|
||||||
|
sharedPreferences.edit {
|
||||||
|
putInt(getString(R.string.set_key_genre_songs_sort), value.intCode)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,15 +24,16 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import org.oxycblt.auxio.list.menu.Menu
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.playback.PlaySong
|
||||||
import org.oxycblt.auxio.util.Event
|
import org.oxycblt.auxio.util.Event
|
||||||
import org.oxycblt.auxio.util.MutableEvent
|
import org.oxycblt.auxio.util.MutableEvent
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
@ -46,10 +47,8 @@ import org.oxycblt.auxio.util.logW
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ListViewModel
|
class ListViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(private val listSettings: ListSettings, private val musicRepository: MusicRepository) :
|
||||||
private val musicRepository: MusicRepository,
|
ViewModel(), MusicRepository.UpdateListener {
|
||||||
private val musicSettings: MusicSettings
|
|
||||||
) : ViewModel(), MusicRepository.UpdateListener {
|
|
||||||
private val _selected = MutableStateFlow(listOf<Music>())
|
private val _selected = MutableStateFlow(listOf<Music>())
|
||||||
/** The currently selected items. These are ordered in earliest selected and latest selected. */
|
/** The currently selected items. These are ordered in earliest selected and latest selected. */
|
||||||
val selected: StateFlow<List<Music>>
|
val selected: StateFlow<List<Music>>
|
||||||
|
|
@ -121,9 +120,9 @@ constructor(
|
||||||
.flatMap {
|
.flatMap {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Song -> listOf(it)
|
is Song -> listOf(it)
|
||||||
is Album -> musicSettings.albumSongSort.songs(it.songs)
|
is Album -> listSettings.albumSongSort.songs(it.songs)
|
||||||
is Artist -> musicSettings.artistSongSort.songs(it.songs)
|
is Artist -> listSettings.artistSongSort.songs(it.songs)
|
||||||
is Genre -> musicSettings.genreSongSort.songs(it.songs)
|
is Genre -> listSettings.genreSongSort.songs(it.songs)
|
||||||
is Playlist -> it.songs
|
is Playlist -> it.songs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -146,10 +145,12 @@ constructor(
|
||||||
*
|
*
|
||||||
* @param menuRes The resource of the menu to use.
|
* @param menuRes The resource of the menu to use.
|
||||||
* @param song The [Song] to show.
|
* @param song The [Song] to show.
|
||||||
|
* @param playWith A [PlaySong] command to give context to what "Play" and "Shuffle" actions
|
||||||
|
* should do.
|
||||||
*/
|
*/
|
||||||
fun openMenu(@MenuRes menuRes: Int, song: Song) {
|
fun openMenu(@MenuRes menuRes: Int, song: Song, playWith: PlaySong) {
|
||||||
logD("Opening menu for $song")
|
logD("Opening menu for $song")
|
||||||
openImpl(Menu.ForSong(menuRes, song))
|
openImpl(Menu.ForSong(menuRes, song, playWith))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -209,26 +210,3 @@ constructor(
|
||||||
_menu.put(menu)
|
_menu.put(menu)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Command to navigate to a specific menu dialog configuration.
|
|
||||||
*
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
|
||||||
*/
|
|
||||||
sealed interface Menu {
|
|
||||||
/** The android resource ID of the menu options to display in the dialog. */
|
|
||||||
val menuRes: Int
|
|
||||||
/** The [Music] that the menu should act on. */
|
|
||||||
val music: Music
|
|
||||||
|
|
||||||
/** Navigate to a [Song] menu dialog. */
|
|
||||||
class ForSong(@MenuRes override val menuRes: Int, override val music: Song) : Menu
|
|
||||||
/** Navigate to a [Album] menu dialog. */
|
|
||||||
class ForAlbum(@MenuRes override val menuRes: Int, override val music: Album) : Menu
|
|
||||||
/** Navigate to a [Artist] menu dialog. */
|
|
||||||
class ForArtist(@MenuRes override val menuRes: Int, override val music: Artist) : Menu
|
|
||||||
/** Navigate to a [Genre] menu dialog. */
|
|
||||||
class ForGenre(@MenuRes override val menuRes: Int, override val music: Genre) : Menu
|
|
||||||
/** Navigate to a [Playlist] menu dialog. */
|
|
||||||
class ForPlaylist(@MenuRes override val menuRes: Int, override val music: Playlist) : Menu
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -115,9 +115,8 @@ interface SelectableListListener<in T> : ClickableListListener<T> {
|
||||||
* Called when an item in the list requests that a menu related to it should be opened.
|
* Called when an item in the list requests that a menu related to it should be opened.
|
||||||
*
|
*
|
||||||
* @param item The [T] item to open a menu for.
|
* @param item The [T] item to open a menu for.
|
||||||
* @param anchor The [View] to anchor the menu to.
|
|
||||||
*/
|
*/
|
||||||
fun onOpenMenu(item: T, anchor: View)
|
fun onOpenMenu(item: T)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when an item in the list requests that it be selected.
|
* Called when an item in the list requests that it be selected.
|
||||||
|
|
@ -148,6 +147,6 @@ interface SelectableListListener<in T> : ClickableListListener<T> {
|
||||||
true
|
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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ abstract class FlexibleListAdapter<T, VH : RecyclerView.ViewHolder>(
|
||||||
diffCallback: DiffUtil.ItemCallback<T>
|
diffCallback: DiffUtil.ItemCallback<T>
|
||||||
) : RecyclerView.Adapter<VH>() {
|
) : RecyclerView.Adapter<VH>() {
|
||||||
@Suppress("LeakingThis") private val differ = FlexibleListDiffer(this, diffCallback)
|
@Suppress("LeakingThis") private val differ = FlexibleListDiffer(this, diffCallback)
|
||||||
|
|
||||||
final override fun getItemCount() = differ.currentList.size
|
final override fun getItemCount() = differ.currentList.size
|
||||||
/** The current list stored by the adapter's differ instance. */
|
/** The current list stored by the adapter's differ instance. */
|
||||||
val currentList: List<T>
|
val currentList: List<T>
|
||||||
|
|
@ -69,7 +70,7 @@ abstract class FlexibleListAdapter<T, VH : RecyclerView.ViewHolder>(
|
||||||
*/
|
*/
|
||||||
sealed interface UpdateInstructions {
|
sealed interface UpdateInstructions {
|
||||||
/** Use an asynchronous diff. Useful for unpredictable updates, but looks chaotic and janky. */
|
/** Use an asynchronous diff. Useful for unpredictable updates, but looks chaotic and janky. */
|
||||||
object Diff : UpdateInstructions
|
data object Diff : UpdateInstructions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visually replace all items from a given point. More visually coherent than [Diff].
|
* Visually replace all items from a given point. More visually coherent than [Diff].
|
||||||
|
|
@ -118,6 +119,7 @@ private class FlexibleListDiffer<T>(
|
||||||
|
|
||||||
private class MainThreadExecutor : Executor {
|
private class MainThreadExecutor : Executor {
|
||||||
val mHandler = Handler(Looper.getMainLooper())
|
val mHandler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
override fun execute(command: Runnable) {
|
override fun execute(command: Runnable) {
|
||||||
mHandler.post(command)
|
mHandler.post(command)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
102
app/src/main/java/org/oxycblt/auxio/list/menu/Menu.kt
Normal file
102
app/src/main/java/org/oxycblt/auxio/list/menu/Menu.kt
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* Menu.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.list.menu
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.annotation.MenuRes
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.oxycblt.auxio.music.Album
|
||||||
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
import org.oxycblt.auxio.music.Music
|
||||||
|
import org.oxycblt.auxio.music.Playlist
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.playback.PlaySong
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command to navigate to a specific menu dialog configuration.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
sealed interface Menu {
|
||||||
|
/** The menu resource to inflate in the menu dialog. */
|
||||||
|
@get:MenuRes val res: Int
|
||||||
|
/** A [Parcel] version of this instance that can be used as a navigation argument. */
|
||||||
|
val parcel: Parcel
|
||||||
|
|
||||||
|
sealed interface Parcel : Parcelable
|
||||||
|
|
||||||
|
/** Navigate to a [Song] menu dialog. */
|
||||||
|
class ForSong(@MenuRes override val res: Int, val song: Song, val playWith: PlaySong) : Menu {
|
||||||
|
override val parcel: Parcel
|
||||||
|
get() {
|
||||||
|
val playWithUid =
|
||||||
|
when (playWith) {
|
||||||
|
is PlaySong.FromArtist -> playWith.which?.uid
|
||||||
|
is PlaySong.FromGenre -> playWith.which?.uid
|
||||||
|
is PlaySong.FromPlaylist -> playWith.which.uid
|
||||||
|
is PlaySong.FromAll,
|
||||||
|
is PlaySong.FromAlbum,
|
||||||
|
is PlaySong.ByItself -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
return Parcel(res, song.uid, playWith.intCode, playWithUid)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Parcel(
|
||||||
|
val res: Int,
|
||||||
|
val songUid: Music.UID,
|
||||||
|
val playWithCode: Int,
|
||||||
|
val playWithUid: Music.UID?
|
||||||
|
) : Menu.Parcel
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Navigate to a [Album] menu dialog. */
|
||||||
|
class ForAlbum(@MenuRes override val res: Int, val album: Album) : Menu {
|
||||||
|
override val parcel
|
||||||
|
get() = Parcel(res, album.uid)
|
||||||
|
|
||||||
|
@Parcelize data class Parcel(val res: Int, val albumUid: Music.UID) : Menu.Parcel
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Navigate to a [Artist] menu dialog. */
|
||||||
|
class ForArtist(@MenuRes override val res: Int, val artist: Artist) : Menu {
|
||||||
|
override val parcel
|
||||||
|
get() = Parcel(res, artist.uid)
|
||||||
|
|
||||||
|
@Parcelize data class Parcel(val res: Int, val artistUid: Music.UID) : Menu.Parcel
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Navigate to a [Genre] menu dialog. */
|
||||||
|
class ForGenre(@MenuRes override val res: Int, val genre: Genre) : Menu {
|
||||||
|
override val parcel
|
||||||
|
get() = Parcel(res, genre.uid)
|
||||||
|
|
||||||
|
@Parcelize data class Parcel(val res: Int, val genreUid: Music.UID) : Menu.Parcel
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Navigate to a [Playlist] menu dialog. */
|
||||||
|
class ForPlaylist(@MenuRes override val res: Int, val playlist: Playlist) : Menu {
|
||||||
|
override val parcel
|
||||||
|
get() = Parcel(res, playlist.uid)
|
||||||
|
|
||||||
|
@Parcelize data class Parcel(val res: Int, val playlistUid: Music.UID) : Menu.Parcel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,7 +31,6 @@ import org.oxycblt.auxio.databinding.DialogMenuBinding
|
||||||
import org.oxycblt.auxio.list.ClickableListListener
|
import org.oxycblt.auxio.list.ClickableListListener
|
||||||
import org.oxycblt.auxio.list.ListViewModel
|
import org.oxycblt.auxio.list.ListViewModel
|
||||||
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||||
import org.oxycblt.auxio.music.Music
|
|
||||||
import org.oxycblt.auxio.ui.ViewBindingBottomSheetDialogFragment
|
import org.oxycblt.auxio.ui.ViewBindingBottomSheetDialogFragment
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
@ -44,40 +43,36 @@ import org.oxycblt.auxio.util.logD
|
||||||
*
|
*
|
||||||
* TODO: Extend the amount of music info shown in the dialog
|
* TODO: Extend the amount of music info shown in the dialog
|
||||||
*/
|
*/
|
||||||
abstract class MenuDialogFragment<T : Music> :
|
abstract class MenuDialogFragment<M : Menu> :
|
||||||
ViewBindingBottomSheetDialogFragment<DialogMenuBinding>(), ClickableListListener<MenuItem> {
|
ViewBindingBottomSheetDialogFragment<DialogMenuBinding>(), ClickableListListener<MenuItem> {
|
||||||
protected abstract val menuModel: MenuViewModel
|
protected abstract val menuModel: MenuViewModel
|
||||||
protected abstract val listModel: ListViewModel
|
protected abstract val listModel: ListViewModel
|
||||||
private val menuAdapter = MenuItemAdapter(@Suppress("LeakingThis") this)
|
private val menuAdapter = MenuItemAdapter(@Suppress("LeakingThis") this)
|
||||||
|
|
||||||
/** The android resource ID of the menu options to display in the dialog. */
|
abstract val parcel: Menu.Parcel
|
||||||
abstract val menuRes: Int
|
|
||||||
|
|
||||||
/** The [Music.UID] of the [T] to display menu options for. */
|
|
||||||
abstract val uid: Music.UID
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the options to disable in the context of the currently shown [T].
|
* Get the options to disable in the context of the currently shown [M].
|
||||||
*
|
*
|
||||||
* @param music The currently-shown music [T].
|
* @param menu The currently-shown menu [M].
|
||||||
*/
|
*/
|
||||||
abstract fun getDisabledItemIds(music: T): Set<Int>
|
abstract fun getDisabledItemIds(menu: M): Set<Int>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the displayed information about the currently shown [T].
|
* Update the displayed information about the currently shown [M].
|
||||||
*
|
*
|
||||||
* @param binding The [DialogMenuBinding] to bind information to.
|
* @param binding The [DialogMenuBinding] to bind information to.
|
||||||
* @param music The currently-shown music [T].
|
* @param menu The currently-shown menu [M].
|
||||||
*/
|
*/
|
||||||
abstract fun updateMusic(binding: DialogMenuBinding, music: T)
|
abstract fun updateMenu(binding: DialogMenuBinding, menu: M)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forward the clicked [MenuItem] to it's corresponding handler in another module.
|
* Forward the clicked [MenuItem] to it's corresponding handler in another module.
|
||||||
*
|
*
|
||||||
* @param item The [MenuItem] that was clicked.
|
* @param item The [MenuItem] that was clicked.
|
||||||
* @param music The currently-shown music [T].
|
* @param menu The currently-shown menu [M].
|
||||||
*/
|
*/
|
||||||
abstract fun onClick(item: MenuItem, music: T)
|
abstract fun onClick(item: MenuItem, menu: M)
|
||||||
|
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) = DialogMenuBinding.inflate(inflater)
|
override fun onCreateBinding(inflater: LayoutInflater) = DialogMenuBinding.inflate(inflater)
|
||||||
|
|
||||||
|
|
@ -94,8 +89,8 @@ abstract class MenuDialogFragment<T : Music> :
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
listModel.menu.consume()
|
listModel.menu.consume()
|
||||||
menuModel.setMusic(uid)
|
menuModel.setMenu(parcel)
|
||||||
collectImmediately(menuModel.currentMusic, this::updateMusic)
|
collectImmediately(menuModel.currentMenu, this::updateMenu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: DialogMenuBinding) {
|
override fun onDestroyBinding(binding: DialogMenuBinding) {
|
||||||
|
|
@ -105,23 +100,25 @@ abstract class MenuDialogFragment<T : Music> :
|
||||||
binding.menuOptionRecycler.adapter = null
|
binding.menuOptionRecycler.adapter = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMusic(music: Music?) {
|
private fun updateMenu(menu: Menu?) {
|
||||||
if (music == null) {
|
if (menu == null) {
|
||||||
logD("No music to show, navigating away")
|
logD("No menu to show, navigating away")
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST") val castedMusic = music as T
|
@Suppress("UNCHECKED_CAST") val casted = menu as? M
|
||||||
|
check(casted != null) { "Unexpected menu instance ${menu::class.simpleName}" }
|
||||||
|
|
||||||
// We need to inflate the menu on every music update since it might have changed
|
// We need to inflate the menu on every menu update since it might have changed
|
||||||
// what options are available (ex. if an artist with no songs has had new songs added).
|
// what options are available (ex. if an artist with no songs has had new songs added).
|
||||||
// Since we don't have (and don't want) a dummy view to inflate this menu, just
|
// Since we don't have (and don't want) a dummy view to inflate this menu, just
|
||||||
// depend on the AndroidX Toolbar internal API and hope for the best.
|
// depend on the AndroidX Toolbar internal API and hope for the best.
|
||||||
@SuppressLint("RestrictedApi") val builder = MenuBuilder(requireContext())
|
@SuppressLint("RestrictedApi") val builder = MenuBuilder(requireContext())
|
||||||
MenuInflater(requireContext()).inflate(menuRes, builder)
|
MenuInflater(requireContext()).inflate(casted.res, builder)
|
||||||
|
|
||||||
// Disable any menu options as specified by the impl
|
// Disable any menu options as specified by the impl
|
||||||
val disabledIds = getDisabledItemIds(castedMusic)
|
val disabledIds = getDisabledItemIds(casted)
|
||||||
val visible =
|
val visible =
|
||||||
builder.children.mapTo(mutableListOf()) {
|
builder.children.mapTo(mutableListOf()) {
|
||||||
it.isEnabled = !disabledIds.contains(it.itemId)
|
it.isEnabled = !disabledIds.contains(it.itemId)
|
||||||
|
|
@ -130,7 +127,7 @@ abstract class MenuDialogFragment<T : Music> :
|
||||||
menuAdapter.update(visible, UpdateInstructions.Diff)
|
menuAdapter.update(visible, UpdateInstructions.Diff)
|
||||||
|
|
||||||
// Delegate to impl how to show music
|
// Delegate to impl how to show music
|
||||||
updateMusic(requireBinding(), castedMusic)
|
updateMenu(requireBinding(), casted)
|
||||||
}
|
}
|
||||||
|
|
||||||
final override fun onClick(item: MenuItem, viewHolder: RecyclerView.ViewHolder) {
|
final override fun onClick(item: MenuItem, viewHolder: RecyclerView.ViewHolder) {
|
||||||
|
|
@ -138,6 +135,6 @@ abstract class MenuDialogFragment<T : Music> :
|
||||||
// TODO: This should change if the app is 100% migrated to menu dialogs
|
// TODO: This should change if the app is 100% migrated to menu dialogs
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
// Delegate to impl on how to handle items
|
// Delegate to impl on how to handle items
|
||||||
@Suppress("UNCHECKED_CAST") onClick(item, menuModel.currentMusic.value as T)
|
@Suppress("UNCHECKED_CAST") onClick(item, menuModel.currentMenu.value as M)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,8 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.DialogMenuBinding
|
import org.oxycblt.auxio.databinding.DialogMenuBinding
|
||||||
import org.oxycblt.auxio.detail.DetailViewModel
|
import org.oxycblt.auxio.detail.DetailViewModel
|
||||||
import org.oxycblt.auxio.list.ListViewModel
|
import org.oxycblt.auxio.list.ListViewModel
|
||||||
import org.oxycblt.auxio.music.Album
|
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Music
|
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
|
@ -46,7 +44,7 @@ import org.oxycblt.auxio.util.showToast
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class SongMenuDialogFragment : MenuDialogFragment<Song>() {
|
class SongMenuDialogFragment : MenuDialogFragment<Menu.ForSong>() {
|
||||||
override val menuModel: MenuViewModel by activityViewModels()
|
override val menuModel: MenuViewModel by activityViewModels()
|
||||||
override val listModel: ListViewModel by activityViewModels()
|
override val listModel: ListViewModel by activityViewModels()
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
@ -54,38 +52,37 @@ class SongMenuDialogFragment : MenuDialogFragment<Song>() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val args: SongMenuDialogFragmentArgs by navArgs()
|
private val args: SongMenuDialogFragmentArgs by navArgs()
|
||||||
|
|
||||||
override val menuRes: Int
|
override val parcel
|
||||||
get() = args.menuRes
|
get() = args.parcel
|
||||||
override val uid: Music.UID
|
|
||||||
get() = args.songUid
|
|
||||||
|
|
||||||
// Nothing to disable in song menus.
|
// Nothing to disable in song menus.
|
||||||
override fun getDisabledItemIds(music: Song) = setOf<Int>()
|
override fun getDisabledItemIds(menu: Menu.ForSong) = setOf<Int>()
|
||||||
|
|
||||||
override fun updateMusic(binding: DialogMenuBinding, music: Song) {
|
override fun updateMenu(binding: DialogMenuBinding, menu: Menu.ForSong) {
|
||||||
val context = requireContext()
|
val context = requireContext()
|
||||||
binding.menuCover.bind(music)
|
binding.menuCover.bind(menu.song)
|
||||||
binding.menuType.text = getString(R.string.lbl_song)
|
binding.menuType.text = getString(R.string.lbl_song)
|
||||||
binding.menuName.text = music.name.resolve(context)
|
binding.menuName.text = menu.song.name.resolve(context)
|
||||||
binding.menuInfo.text = music.artists.resolveNames(context)
|
binding.menuInfo.text = menu.song.artists.resolveNames(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(item: MenuItem, music: Song) {
|
override fun onClick(item: MenuItem, menu: Menu.ForSong) {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
// TODO: Song play and shuffle as soon as PlaybackMode is refactored
|
R.id.action_play -> playbackModel.playExplicit(menu.song, menu.playWith)
|
||||||
|
R.id.action_shuffle -> playbackModel.shuffleExplicit(menu.song, menu.playWith)
|
||||||
R.id.action_play_next -> {
|
R.id.action_play_next -> {
|
||||||
playbackModel.playNext(music)
|
playbackModel.playNext(menu.song)
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
}
|
}
|
||||||
R.id.action_queue_add -> {
|
R.id.action_queue_add -> {
|
||||||
playbackModel.addToQueue(music)
|
playbackModel.addToQueue(menu.song)
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
}
|
}
|
||||||
R.id.action_artist_details -> detailModel.showArtist(music)
|
R.id.action_artist_details -> detailModel.showArtist(menu.song)
|
||||||
R.id.action_album_details -> detailModel.showAlbum(music)
|
R.id.action_album_details -> detailModel.showAlbum(menu.song.album)
|
||||||
R.id.action_share -> requireContext().share(music)
|
R.id.action_share -> requireContext().share(menu.song)
|
||||||
R.id.action_playlist_add -> musicModel.addToPlaylist(music)
|
R.id.action_playlist_add -> musicModel.addToPlaylist(menu.song)
|
||||||
R.id.action_detail -> detailModel.showSong(music)
|
R.id.action_detail -> detailModel.showSong(menu.song)
|
||||||
else -> error("Unexpected menu item selected $item")
|
else -> error("Unexpected menu item selected $item")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -97,7 +94,7 @@ class SongMenuDialogFragment : MenuDialogFragment<Song>() {
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class AlbumMenuDialogFragment : MenuDialogFragment<Album>() {
|
class AlbumMenuDialogFragment : MenuDialogFragment<Menu.ForAlbum>() {
|
||||||
override val menuModel: MenuViewModel by viewModels()
|
override val menuModel: MenuViewModel by viewModels()
|
||||||
override val listModel: ListViewModel by activityViewModels()
|
override val listModel: ListViewModel by activityViewModels()
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
@ -105,38 +102,36 @@ class AlbumMenuDialogFragment : MenuDialogFragment<Album>() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val args: AlbumMenuDialogFragmentArgs by navArgs()
|
private val args: AlbumMenuDialogFragmentArgs by navArgs()
|
||||||
|
|
||||||
override val menuRes: Int
|
override val parcel
|
||||||
get() = args.menuRes
|
get() = args.parcel
|
||||||
override val uid: Music.UID
|
|
||||||
get() = args.albumUid
|
|
||||||
|
|
||||||
// Nothing to disable in album menus.
|
// Nothing to disable in album menus.
|
||||||
override fun getDisabledItemIds(music: Album) = setOf<Int>()
|
override fun getDisabledItemIds(menu: Menu.ForAlbum) = setOf<Int>()
|
||||||
|
|
||||||
override fun updateMusic(binding: DialogMenuBinding, music: Album) {
|
override fun updateMenu(binding: DialogMenuBinding, menu: Menu.ForAlbum) {
|
||||||
val context = requireContext()
|
val context = requireContext()
|
||||||
binding.menuCover.bind(music)
|
binding.menuCover.bind(menu.album)
|
||||||
binding.menuType.text = getString(music.releaseType.stringRes)
|
binding.menuType.text = getString(menu.album.releaseType.stringRes)
|
||||||
binding.menuName.text = music.name.resolve(context)
|
binding.menuName.text = menu.album.name.resolve(context)
|
||||||
binding.menuInfo.text = music.artists.resolveNames(context)
|
binding.menuInfo.text = menu.album.artists.resolveNames(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(item: MenuItem, music: Album) {
|
override fun onClick(item: MenuItem, menu: Menu.ForAlbum) {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_play -> playbackModel.play(music)
|
R.id.action_play -> playbackModel.play(menu.album)
|
||||||
R.id.action_shuffle -> playbackModel.shuffle(music)
|
R.id.action_shuffle -> playbackModel.shuffle(menu.album)
|
||||||
R.id.action_detail -> detailModel.showAlbum(music)
|
R.id.action_detail -> detailModel.showAlbum(menu.album)
|
||||||
R.id.action_play_next -> {
|
R.id.action_play_next -> {
|
||||||
playbackModel.playNext(music)
|
playbackModel.playNext(menu.album)
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
}
|
}
|
||||||
R.id.action_queue_add -> {
|
R.id.action_queue_add -> {
|
||||||
playbackModel.addToQueue(music)
|
playbackModel.addToQueue(menu.album)
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
}
|
}
|
||||||
R.id.action_artist_details -> detailModel.showArtist(music)
|
R.id.action_artist_details -> detailModel.showArtist(menu.album)
|
||||||
R.id.action_playlist_add -> musicModel.addToPlaylist(music)
|
R.id.action_playlist_add -> musicModel.addToPlaylist(menu.album)
|
||||||
R.id.action_share -> requireContext().share(music)
|
R.id.action_share -> requireContext().share(menu.album)
|
||||||
else -> error("Unexpected menu item selected $item")
|
else -> error("Unexpected menu item selected $item")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -148,7 +143,7 @@ class AlbumMenuDialogFragment : MenuDialogFragment<Album>() {
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ArtistMenuDialogFragment : MenuDialogFragment<Artist>() {
|
class ArtistMenuDialogFragment : MenuDialogFragment<Menu.ForArtist>() {
|
||||||
override val menuModel: MenuViewModel by viewModels()
|
override val menuModel: MenuViewModel by viewModels()
|
||||||
override val listModel: ListViewModel by activityViewModels()
|
override val listModel: ListViewModel by activityViewModels()
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
@ -156,13 +151,11 @@ class ArtistMenuDialogFragment : MenuDialogFragment<Artist>() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val args: ArtistMenuDialogFragmentArgs by navArgs()
|
private val args: ArtistMenuDialogFragmentArgs by navArgs()
|
||||||
|
|
||||||
override val menuRes: Int
|
override val parcel
|
||||||
get() = args.menuRes
|
get() = args.parcel
|
||||||
override val uid: Music.UID
|
|
||||||
get() = args.artistUid
|
|
||||||
|
|
||||||
override fun getDisabledItemIds(music: Artist) =
|
override fun getDisabledItemIds(menu: Menu.ForArtist) =
|
||||||
if (music.songs.isEmpty()) {
|
if (menu.artist.songs.isEmpty()) {
|
||||||
// Disable any operations that require some kind of songs to work with, as there won't
|
// Disable any operations that require some kind of songs to work with, as there won't
|
||||||
// be any in an empty artist.
|
// be any in an empty artist.
|
||||||
setOf(
|
setOf(
|
||||||
|
|
@ -176,37 +169,37 @@ class ArtistMenuDialogFragment : MenuDialogFragment<Artist>() {
|
||||||
setOf()
|
setOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateMusic(binding: DialogMenuBinding, music: Artist) {
|
override fun updateMenu(binding: DialogMenuBinding, menu: Menu.ForArtist) {
|
||||||
val context = requireContext()
|
val context = requireContext()
|
||||||
binding.menuCover.bind(music)
|
binding.menuCover.bind(menu.artist)
|
||||||
binding.menuType.text = getString(R.string.lbl_artist)
|
binding.menuType.text = getString(R.string.lbl_artist)
|
||||||
binding.menuName.text = music.name.resolve(context)
|
binding.menuName.text = menu.artist.name.resolve(context)
|
||||||
binding.menuInfo.text =
|
binding.menuInfo.text =
|
||||||
getString(
|
getString(
|
||||||
R.string.fmt_two,
|
R.string.fmt_two,
|
||||||
context.getPlural(R.plurals.fmt_album_count, music.albums.size),
|
context.getPlural(R.plurals.fmt_album_count, menu.artist.albums.size),
|
||||||
if (music.songs.isNotEmpty()) {
|
if (menu.artist.songs.isNotEmpty()) {
|
||||||
context.getPlural(R.plurals.fmt_song_count, music.songs.size)
|
context.getPlural(R.plurals.fmt_song_count, menu.artist.songs.size)
|
||||||
} else {
|
} else {
|
||||||
getString(R.string.def_song_count)
|
getString(R.string.def_song_count)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(item: MenuItem, music: Artist) {
|
override fun onClick(item: MenuItem, menu: Menu.ForArtist) {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_play -> playbackModel.play(music)
|
R.id.action_play -> playbackModel.play(menu.artist)
|
||||||
R.id.action_shuffle -> playbackModel.shuffle(music)
|
R.id.action_shuffle -> playbackModel.shuffle(menu.artist)
|
||||||
R.id.action_detail -> detailModel.showArtist(music)
|
R.id.action_detail -> detailModel.showArtist(menu.artist)
|
||||||
R.id.action_play_next -> {
|
R.id.action_play_next -> {
|
||||||
playbackModel.playNext(music)
|
playbackModel.playNext(menu.artist)
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
}
|
}
|
||||||
R.id.action_queue_add -> {
|
R.id.action_queue_add -> {
|
||||||
playbackModel.addToQueue(music)
|
playbackModel.addToQueue(menu.artist)
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
}
|
}
|
||||||
R.id.action_playlist_add -> musicModel.addToPlaylist(music)
|
R.id.action_playlist_add -> musicModel.addToPlaylist(menu.artist)
|
||||||
R.id.action_share -> requireContext().share(music)
|
R.id.action_share -> requireContext().share(menu.artist)
|
||||||
else -> error("Unexpected menu item $item")
|
else -> error("Unexpected menu item $item")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -218,7 +211,7 @@ class ArtistMenuDialogFragment : MenuDialogFragment<Artist>() {
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class GenreMenuDialogFragment : MenuDialogFragment<Genre>() {
|
class GenreMenuDialogFragment : MenuDialogFragment<Menu.ForGenre>() {
|
||||||
override val menuModel: MenuViewModel by viewModels()
|
override val menuModel: MenuViewModel by viewModels()
|
||||||
override val listModel: ListViewModel by activityViewModels()
|
override val listModel: ListViewModel by activityViewModels()
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
@ -226,40 +219,38 @@ class GenreMenuDialogFragment : MenuDialogFragment<Genre>() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val args: GenreMenuDialogFragmentArgs by navArgs()
|
private val args: GenreMenuDialogFragmentArgs by navArgs()
|
||||||
|
|
||||||
override val menuRes: Int
|
override val parcel
|
||||||
get() = args.menuRes
|
get() = args.parcel
|
||||||
override val uid: Music.UID
|
|
||||||
get() = args.genreUid
|
|
||||||
|
|
||||||
override fun getDisabledItemIds(music: Genre) = setOf<Int>()
|
override fun getDisabledItemIds(menu: Menu.ForGenre) = setOf<Int>()
|
||||||
|
|
||||||
override fun updateMusic(binding: DialogMenuBinding, music: Genre) {
|
override fun updateMenu(binding: DialogMenuBinding, menu: Menu.ForGenre) {
|
||||||
val context = requireContext()
|
val context = requireContext()
|
||||||
binding.menuCover.bind(music)
|
binding.menuCover.bind(menu.genre)
|
||||||
binding.menuType.text = getString(R.string.lbl_genre)
|
binding.menuType.text = getString(R.string.lbl_genre)
|
||||||
binding.menuName.text = music.name.resolve(context)
|
binding.menuName.text = menu.genre.name.resolve(context)
|
||||||
binding.menuInfo.text =
|
binding.menuInfo.text =
|
||||||
getString(
|
getString(
|
||||||
R.string.fmt_two,
|
R.string.fmt_two,
|
||||||
context.getPlural(R.plurals.fmt_artist_count, music.artists.size),
|
context.getPlural(R.plurals.fmt_artist_count, menu.genre.artists.size),
|
||||||
context.getPlural(R.plurals.fmt_song_count, music.songs.size))
|
context.getPlural(R.plurals.fmt_song_count, menu.genre.songs.size))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(item: MenuItem, music: Genre) {
|
override fun onClick(item: MenuItem, menu: Menu.ForGenre) {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_play -> playbackModel.play(music)
|
R.id.action_play -> playbackModel.play(menu.genre)
|
||||||
R.id.action_shuffle -> playbackModel.shuffle(music)
|
R.id.action_shuffle -> playbackModel.shuffle(menu.genre)
|
||||||
R.id.action_detail -> detailModel.showGenre(music)
|
R.id.action_detail -> detailModel.showGenre(menu.genre)
|
||||||
R.id.action_play_next -> {
|
R.id.action_play_next -> {
|
||||||
playbackModel.playNext(music)
|
playbackModel.playNext(menu.genre)
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
}
|
}
|
||||||
R.id.action_queue_add -> {
|
R.id.action_queue_add -> {
|
||||||
playbackModel.addToQueue(music)
|
playbackModel.addToQueue(menu.genre)
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
}
|
}
|
||||||
R.id.action_playlist_add -> musicModel.addToPlaylist(music)
|
R.id.action_playlist_add -> musicModel.addToPlaylist(menu.genre)
|
||||||
R.id.action_share -> requireContext().share(music)
|
R.id.action_share -> requireContext().share(menu.genre)
|
||||||
else -> error("Unexpected menu item $item")
|
else -> error("Unexpected menu item $item")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -271,7 +262,7 @@ class GenreMenuDialogFragment : MenuDialogFragment<Genre>() {
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class PlaylistMenuDialogFragment : MenuDialogFragment<Playlist>() {
|
class PlaylistMenuDialogFragment : MenuDialogFragment<Menu.ForPlaylist>() {
|
||||||
override val menuModel: MenuViewModel by viewModels()
|
override val menuModel: MenuViewModel by viewModels()
|
||||||
override val listModel: ListViewModel by activityViewModels()
|
override val listModel: ListViewModel by activityViewModels()
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
@ -279,13 +270,11 @@ class PlaylistMenuDialogFragment : MenuDialogFragment<Playlist>() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val args: PlaylistMenuDialogFragmentArgs by navArgs()
|
private val args: PlaylistMenuDialogFragmentArgs by navArgs()
|
||||||
|
|
||||||
override val menuRes: Int
|
override val parcel
|
||||||
get() = args.menuRes
|
get() = args.parcel
|
||||||
override val uid: Music.UID
|
|
||||||
get() = args.playlistUid
|
|
||||||
|
|
||||||
override fun getDisabledItemIds(music: Playlist) =
|
override fun getDisabledItemIds(menu: Menu.ForPlaylist) =
|
||||||
if (music.songs.isEmpty()) {
|
if (menu.playlist.songs.isEmpty()) {
|
||||||
// Disable any operations that require some kind of songs to work with, as there won't
|
// Disable any operations that require some kind of songs to work with, as there won't
|
||||||
// be any in an empty playlist.
|
// be any in an empty playlist.
|
||||||
setOf(
|
setOf(
|
||||||
|
|
@ -299,35 +288,35 @@ class PlaylistMenuDialogFragment : MenuDialogFragment<Playlist>() {
|
||||||
setOf()
|
setOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateMusic(binding: DialogMenuBinding, music: Playlist) {
|
override fun updateMenu(binding: DialogMenuBinding, menu: Menu.ForPlaylist) {
|
||||||
val context = requireContext()
|
val context = requireContext()
|
||||||
binding.menuCover.bind(music)
|
binding.menuCover.bind(menu.playlist)
|
||||||
binding.menuType.text = getString(R.string.lbl_playlist)
|
binding.menuType.text = getString(R.string.lbl_playlist)
|
||||||
binding.menuName.text = music.name.resolve(context)
|
binding.menuName.text = menu.playlist.name.resolve(context)
|
||||||
binding.menuInfo.text =
|
binding.menuInfo.text =
|
||||||
if (music.songs.isNotEmpty()) {
|
if (menu.playlist.songs.isNotEmpty()) {
|
||||||
context.getPlural(R.plurals.fmt_song_count, music.songs.size)
|
context.getPlural(R.plurals.fmt_song_count, menu.playlist.songs.size)
|
||||||
} else {
|
} else {
|
||||||
getString(R.string.def_song_count)
|
getString(R.string.def_song_count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(item: MenuItem, music: Playlist) {
|
override fun onClick(item: MenuItem, menu: Menu.ForPlaylist) {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_play -> playbackModel.play(music)
|
R.id.action_play -> playbackModel.play(menu.playlist)
|
||||||
R.id.action_shuffle -> playbackModel.shuffle(music)
|
R.id.action_shuffle -> playbackModel.shuffle(menu.playlist)
|
||||||
R.id.action_detail -> detailModel.showPlaylist(music)
|
R.id.action_detail -> detailModel.showPlaylist(menu.playlist)
|
||||||
R.id.action_play_next -> {
|
R.id.action_play_next -> {
|
||||||
playbackModel.playNext(music)
|
playbackModel.playNext(menu.playlist)
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
}
|
}
|
||||||
R.id.action_queue_add -> {
|
R.id.action_queue_add -> {
|
||||||
playbackModel.addToQueue(music)
|
playbackModel.addToQueue(menu.playlist)
|
||||||
requireContext().showToast(R.string.lng_queue_added)
|
requireContext().showToast(R.string.lng_queue_added)
|
||||||
}
|
}
|
||||||
R.id.action_rename -> musicModel.renamePlaylist(music)
|
R.id.action_rename -> musicModel.renamePlaylist(menu.playlist)
|
||||||
R.id.action_delete -> musicModel.deletePlaylist(music)
|
R.id.action_delete -> musicModel.deletePlaylist(menu.playlist)
|
||||||
R.id.action_share -> requireContext().share(music)
|
R.id.action_share -> requireContext().share(menu.playlist)
|
||||||
else -> error("Unexpected menu item $item")
|
else -> error("Unexpected menu item $item")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ class MenuItemAdapter(private val listener: ClickableListListener<MenuItem>) :
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [DialogRecyclerView.ViewHolder] that displays a list of menu options based on [MenuItem].
|
* A [DialogRecyclerView.ViewHolder] that displays a [MenuItem].
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
|
|
@ -54,7 +54,7 @@ class MenuItemViewHolder private constructor(private val binding: ItemMenuOption
|
||||||
* Bind new data to this instance.
|
* Bind new data to this instance.
|
||||||
*
|
*
|
||||||
* @param item The new [MenuItem] to bind.
|
* @param item The new [MenuItem] to bind.
|
||||||
* @param listener An [ClickableListListener] to bind interactions to.
|
* @param listener A [ClickableListListener] to bind interactions to.
|
||||||
*/
|
*/
|
||||||
fun bind(item: MenuItem, listener: ClickableListListener<MenuItem>) {
|
fun bind(item: MenuItem, listener: ClickableListListener<MenuItem>) {
|
||||||
listener.bind(item, this)
|
listener.bind(item, this)
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
|
import org.oxycblt.auxio.playback.PlaySong
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -35,32 +36,62 @@ import org.oxycblt.auxio.util.logW
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MenuViewModel @Inject constructor(private val musicRepository: MusicRepository) :
|
class MenuViewModel @Inject constructor(private val musicRepository: MusicRepository) :
|
||||||
ViewModel(), MusicRepository.UpdateListener {
|
ViewModel(), MusicRepository.UpdateListener {
|
||||||
private val _currentMusic = MutableStateFlow<Music?>(null)
|
private val _currentMenu = MutableStateFlow<Menu?>(null)
|
||||||
/** The current [Music] information being shown in a menu dialog. */
|
/** The current [Menu] information being shown in a dialog. */
|
||||||
val currentMusic: StateFlow<Music?> = _currentMusic
|
val currentMenu: StateFlow<Menu?> = _currentMenu
|
||||||
|
|
||||||
init {
|
init {
|
||||||
musicRepository.addUpdateListener(this)
|
musicRepository.addUpdateListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
||||||
_currentMusic.value = _currentMusic.value?.let { musicRepository.find(it.uid) }
|
_currentMenu.value = _currentMenu.value?.let { unpackParcel(it.parcel) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
musicRepository.removeUpdateListener(this)
|
musicRepository.removeUpdateListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun setMenu(parcel: Menu.Parcel) {
|
||||||
* Set a new [currentMusic] from it's [Music.UID]. [currentMusic] will be updated to align with
|
_currentMenu.value = unpackParcel(parcel)
|
||||||
* the new album.
|
if (_currentMenu.value == null) {
|
||||||
*
|
logW("Given menu parcel $parcel was invalid")
|
||||||
* @param uid The [Music.UID] of the [Music] to update [currentMusic] to. Must be valid.
|
|
||||||
*/
|
|
||||||
fun setMusic(uid: Music.UID) {
|
|
||||||
_currentMusic.value = musicRepository.find(uid)
|
|
||||||
if (_currentMusic.value == null) {
|
|
||||||
logW("Given Music UID to show was invalid")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun unpackParcel(parcel: Menu.Parcel) =
|
||||||
|
when (parcel) {
|
||||||
|
is Menu.ForSong.Parcel -> unpackSongParcel(parcel)
|
||||||
|
is Menu.ForAlbum.Parcel -> unpackAlbumParcel(parcel)
|
||||||
|
is Menu.ForArtist.Parcel -> unpackArtistParcel(parcel)
|
||||||
|
is Menu.ForGenre.Parcel -> unpackGenreParcel(parcel)
|
||||||
|
is Menu.ForPlaylist.Parcel -> unpackPlaylistParcel(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unpackSongParcel(parcel: Menu.ForSong.Parcel): Menu.ForSong? {
|
||||||
|
val song = musicRepository.deviceLibrary?.findSong(parcel.songUid) ?: return null
|
||||||
|
val parent = parcel.playWithUid?.let(musicRepository::find) as MusicParent?
|
||||||
|
val playWith = PlaySong.fromIntCode(parcel.playWithCode, parent) ?: return null
|
||||||
|
return Menu.ForSong(parcel.res, song, playWith)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unpackAlbumParcel(parcel: Menu.ForAlbum.Parcel): Menu.ForAlbum? {
|
||||||
|
val album = musicRepository.deviceLibrary?.findAlbum(parcel.albumUid) ?: return null
|
||||||
|
return Menu.ForAlbum(parcel.res, album)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unpackArtistParcel(parcel: Menu.ForArtist.Parcel): Menu.ForArtist? {
|
||||||
|
val artist = musicRepository.deviceLibrary?.findArtist(parcel.artistUid) ?: return null
|
||||||
|
return Menu.ForArtist(parcel.res, artist)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unpackGenreParcel(parcel: Menu.ForGenre.Parcel): Menu.ForGenre? {
|
||||||
|
val genre = musicRepository.deviceLibrary?.findGenre(parcel.genreUid) ?: return null
|
||||||
|
return Menu.ForGenre(parcel.res, genre)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unpackPlaylistParcel(parcel: Menu.ForPlaylist.Parcel): Menu.ForPlaylist? {
|
||||||
|
val playlist = musicRepository.userLibrary?.findPlaylist(parcel.playlistUid) ?: return null
|
||||||
|
return Menu.ForPlaylist(parcel.res, playlist)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowInsets
|
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
|
|
@ -32,7 +31,6 @@ import com.google.android.material.divider.MaterialDivider
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.list.recycler.DialogRecyclerView.ViewHolder
|
import org.oxycblt.auxio.list.recycler.DialogRecyclerView.ViewHolder
|
||||||
import org.oxycblt.auxio.util.getDimenPixels
|
import org.oxycblt.auxio.util.getDimenPixels
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [RecyclerView] intended for use in Dialogs, adding features such as:
|
* A [RecyclerView] intended for use in Dialogs, adding features such as:
|
||||||
|
|
@ -76,13 +74,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
invalidateDividers()
|
invalidateDividers()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
|
||||||
// Update the RecyclerView's padding such that the bottom insets are applied
|
|
||||||
// while still preserving bottom padding.
|
|
||||||
updatePadding(bottom = insets.systemBarInsetsCompat.bottom)
|
|
||||||
return insets
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onScrolled(dx: Int, dy: Int) {
|
override fun onScrolled(dx: Int, dy: Int) {
|
||||||
super.onScrolled(dx, dy)
|
super.onScrolled(dx, dy)
|
||||||
// Scroll event occurred, need to update the dividers.
|
// Scroll event occurred, need to update the dividers.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2023 Auxio Project
|
||||||
* Sort.kt is part of Auxio.
|
* Sort.kt is part of Auxio.
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
|
@ -16,9 +16,8 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.list
|
package org.oxycblt.auxio.list.sort
|
||||||
|
|
||||||
import androidx.annotation.IdRes
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
|
@ -26,7 +25,6 @@ import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.info.Date
|
import org.oxycblt.auxio.music.info.Date
|
||||||
|
|
@ -42,22 +40,6 @@ import org.oxycblt.auxio.music.info.Disc
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
data class Sort(val mode: Mode, val direction: Direction) {
|
data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
/**
|
|
||||||
* Create a new [Sort] with the same [mode], but a different [Direction].
|
|
||||||
*
|
|
||||||
* @param direction The new [Direction] to sort in.
|
|
||||||
* @return A new sort with the same mode, but with the new [Direction] value applied.
|
|
||||||
*/
|
|
||||||
fun withDirection(direction: Direction) = Sort(mode, direction)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new [Sort] with the same [direction] value, but different [mode] value.
|
|
||||||
*
|
|
||||||
* @param mode Tbe new mode to use for the Sort.
|
|
||||||
* @return A new sort with the same [direction] value, but with the new [mode] applied.
|
|
||||||
*/
|
|
||||||
fun withMode(mode: Mode) = Sort(mode, direction)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort a list of [Song]s.
|
* Sort a list of [Song]s.
|
||||||
*
|
*
|
||||||
|
|
@ -163,8 +145,8 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
sealed interface Mode {
|
sealed interface Mode {
|
||||||
/** The integer representation of this sort mode. */
|
/** The integer representation of this sort mode. */
|
||||||
val intCode: Int
|
val intCode: Int
|
||||||
/** The item ID of this sort mode in menu resources. */
|
/** The string resource of the human-readable name of this sort mode. */
|
||||||
val itemId: Int
|
val stringRes: Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a [Comparator] that sorts [Song]s according to this [Mode].
|
* Get a [Comparator] that sorts [Song]s according to this [Mode].
|
||||||
|
|
@ -216,12 +198,12 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
*
|
*
|
||||||
* @see Music.name
|
* @see Music.name
|
||||||
*/
|
*/
|
||||||
object ByName : Mode {
|
data object ByName : Mode {
|
||||||
override val intCode: Int
|
override val intCode: Int
|
||||||
get() = IntegerTable.SORT_BY_NAME
|
get() = IntegerTable.SORT_BY_NAME
|
||||||
|
|
||||||
override val itemId: Int
|
override val stringRes: Int
|
||||||
get() = R.id.option_sort_name
|
get() = R.string.lbl_name
|
||||||
|
|
||||||
override fun getSongComparator(direction: Direction) =
|
override fun getSongComparator(direction: Direction) =
|
||||||
compareByDynamic(direction, BasicComparator.SONG)
|
compareByDynamic(direction, BasicComparator.SONG)
|
||||||
|
|
@ -244,12 +226,12 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
*
|
*
|
||||||
* @see Album.name
|
* @see Album.name
|
||||||
*/
|
*/
|
||||||
object ByAlbum : Mode {
|
data object ByAlbum : Mode {
|
||||||
override val intCode: Int
|
override val intCode: Int
|
||||||
get() = IntegerTable.SORT_BY_ALBUM
|
get() = IntegerTable.SORT_BY_ALBUM
|
||||||
|
|
||||||
override val itemId: Int
|
override val stringRes: Int
|
||||||
get() = R.id.option_sort_album
|
get() = R.string.lbl_album
|
||||||
|
|
||||||
override fun getSongComparator(direction: Direction): Comparator<Song> =
|
override fun getSongComparator(direction: Direction): Comparator<Song> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
|
|
@ -264,12 +246,12 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
*
|
*
|
||||||
* @see Artist.name
|
* @see Artist.name
|
||||||
*/
|
*/
|
||||||
object ByArtist : Mode {
|
data object ByArtist : Mode {
|
||||||
override val intCode: Int
|
override val intCode: Int
|
||||||
get() = IntegerTable.SORT_BY_ARTIST
|
get() = IntegerTable.SORT_BY_ARTIST
|
||||||
|
|
||||||
override val itemId: Int
|
override val stringRes: Int
|
||||||
get() = R.id.option_sort_artist
|
get() = R.string.lbl_artist
|
||||||
|
|
||||||
override fun getSongComparator(direction: Direction): Comparator<Song> =
|
override fun getSongComparator(direction: Direction): Comparator<Song> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
|
|
@ -293,12 +275,12 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
* @see Song.date
|
* @see Song.date
|
||||||
* @see Album.dates
|
* @see Album.dates
|
||||||
*/
|
*/
|
||||||
object ByDate : Mode {
|
data object ByDate : Mode {
|
||||||
override val intCode: Int
|
override val intCode: Int
|
||||||
get() = IntegerTable.SORT_BY_YEAR
|
get() = IntegerTable.SORT_BY_YEAR
|
||||||
|
|
||||||
override val itemId: Int
|
override val stringRes: Int
|
||||||
get() = R.id.option_sort_year
|
get() = R.string.lbl_date
|
||||||
|
|
||||||
override fun getSongComparator(direction: Direction): Comparator<Song> =
|
override fun getSongComparator(direction: Direction): Comparator<Song> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
|
|
@ -315,12 +297,12 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sort by the duration of an item. */
|
/** Sort by the duration of an item. */
|
||||||
object ByDuration : Mode {
|
data object ByDuration : Mode {
|
||||||
override val intCode: Int
|
override val intCode: Int
|
||||||
get() = IntegerTable.SORT_BY_DURATION
|
get() = IntegerTable.SORT_BY_DURATION
|
||||||
|
|
||||||
override val itemId: Int
|
override val stringRes: Int
|
||||||
get() = R.id.option_sort_duration
|
get() = R.string.lbl_duration
|
||||||
|
|
||||||
override fun getSongComparator(direction: Direction): Comparator<Song> =
|
override fun getSongComparator(direction: Direction): Comparator<Song> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
|
|
@ -345,17 +327,13 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
compareBy(BasicComparator.PLAYLIST))
|
compareBy(BasicComparator.PLAYLIST))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Sort by the amount of songs an item contains. Only available for MusicParents. */
|
||||||
* Sort by the amount of songs an item contains. Only available for [MusicParent]s.
|
data object ByCount : Mode {
|
||||||
*
|
|
||||||
* @see MusicParent.songs
|
|
||||||
*/
|
|
||||||
object ByCount : Mode {
|
|
||||||
override val intCode: Int
|
override val intCode: Int
|
||||||
get() = IntegerTable.SORT_BY_COUNT
|
get() = IntegerTable.SORT_BY_COUNT
|
||||||
|
|
||||||
override val itemId: Int
|
override val stringRes: Int
|
||||||
get() = R.id.option_sort_count
|
get() = R.string.lbl_song_count
|
||||||
|
|
||||||
override fun getAlbumComparator(direction: Direction): Comparator<Album> =
|
override fun getAlbumComparator(direction: Direction): Comparator<Album> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
|
|
@ -381,12 +359,12 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
*
|
*
|
||||||
* @see Song.disc
|
* @see Song.disc
|
||||||
*/
|
*/
|
||||||
object ByDisc : Mode {
|
data object ByDisc : Mode {
|
||||||
override val intCode: Int
|
override val intCode: Int
|
||||||
get() = IntegerTable.SORT_BY_DISC
|
get() = IntegerTable.SORT_BY_DISC
|
||||||
|
|
||||||
override val itemId: Int
|
override val stringRes: Int
|
||||||
get() = R.id.option_sort_disc
|
get() = R.string.lbl_disc
|
||||||
|
|
||||||
override fun getSongComparator(direction: Direction): Comparator<Song> =
|
override fun getSongComparator(direction: Direction): Comparator<Song> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
|
|
@ -400,12 +378,12 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
*
|
*
|
||||||
* @see Song.track
|
* @see Song.track
|
||||||
*/
|
*/
|
||||||
object ByTrack : Mode {
|
data object ByTrack : Mode {
|
||||||
override val intCode: Int
|
override val intCode: Int
|
||||||
get() = IntegerTable.SORT_BY_TRACK
|
get() = IntegerTable.SORT_BY_TRACK
|
||||||
|
|
||||||
override val itemId: Int
|
override val stringRes: Int
|
||||||
get() = R.id.option_sort_track
|
get() = R.string.lbl_track
|
||||||
|
|
||||||
override fun getSongComparator(direction: Direction): Comparator<Song> =
|
override fun getSongComparator(direction: Direction): Comparator<Song> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
|
|
@ -420,12 +398,12 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
* @see Song.dateAdded
|
* @see Song.dateAdded
|
||||||
* @see Album.dates
|
* @see Album.dates
|
||||||
*/
|
*/
|
||||||
object ByDateAdded : Mode {
|
data object ByDateAdded : Mode {
|
||||||
override val intCode: Int
|
override val intCode: Int
|
||||||
get() = IntegerTable.SORT_BY_DATE_ADDED
|
get() = IntegerTable.SORT_BY_DATE_ADDED
|
||||||
|
|
||||||
override val itemId: Int
|
override val stringRes: Int
|
||||||
get() = R.id.option_sort_date_added
|
get() = R.string.lbl_date_added
|
||||||
|
|
||||||
override fun getSongComparator(direction: Direction): Comparator<Song> =
|
override fun getSongComparator(direction: Direction): Comparator<Song> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
|
|
@ -458,27 +436,6 @@ data class Sort(val mode: Mode, val direction: Direction) {
|
||||||
ByDateAdded.intCode -> ByDateAdded
|
ByDateAdded.intCode -> ByDateAdded
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a menu item ID into a [Mode].
|
|
||||||
*
|
|
||||||
* @param itemId The menu resource ID to convert
|
|
||||||
* @return A [Mode] corresponding to the given ID, or null if the ID is invalid.
|
|
||||||
* @see itemId
|
|
||||||
*/
|
|
||||||
fun fromItemId(@IdRes itemId: Int) =
|
|
||||||
when (itemId) {
|
|
||||||
ByName.itemId -> ByName
|
|
||||||
ByAlbum.itemId -> ByAlbum
|
|
||||||
ByArtist.itemId -> ByArtist
|
|
||||||
ByDate.itemId -> ByDate
|
|
||||||
ByDuration.itemId -> ByDuration
|
|
||||||
ByCount.itemId -> ByCount
|
|
||||||
ByDisc.itemId -> ByDisc
|
|
||||||
ByTrack.itemId -> ByTrack
|
|
||||||
ByDateAdded.itemId -> ByDateAdded
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
113
app/src/main/java/org/oxycblt/auxio/list/sort/SortDialog.kt
Normal file
113
app/src/main/java/org/oxycblt/auxio/list/sort/SortDialog.kt
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* SortDialog.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.list.sort
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.button.MaterialButtonToggleGroup
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.databinding.DialogSortBinding
|
||||||
|
import org.oxycblt.auxio.list.ClickableListListener
|
||||||
|
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||||
|
import org.oxycblt.auxio.ui.ViewBindingBottomSheetDialogFragment
|
||||||
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
|
||||||
|
abstract class SortDialog :
|
||||||
|
ViewBindingBottomSheetDialogFragment<DialogSortBinding>(),
|
||||||
|
ClickableListListener<Sort.Mode>,
|
||||||
|
MaterialButtonToggleGroup.OnButtonCheckedListener {
|
||||||
|
private val modeAdapter = SortModeAdapter(@Suppress("LeakingThis") this)
|
||||||
|
|
||||||
|
abstract fun getInitialSort(): Sort?
|
||||||
|
|
||||||
|
abstract fun applyChosenSort(sort: Sort)
|
||||||
|
|
||||||
|
abstract fun getModeChoices(): List<Sort.Mode>
|
||||||
|
|
||||||
|
override fun onCreateBinding(inflater: LayoutInflater) = DialogSortBinding.inflate(inflater)
|
||||||
|
|
||||||
|
override fun onBindingCreated(binding: DialogSortBinding, savedInstanceState: Bundle?) {
|
||||||
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
|
// --- UI SETUP ---
|
||||||
|
binding.root.setOnApplyWindowInsetsListener { v, insets ->
|
||||||
|
v.updatePadding(bottom = insets.systemBarInsetsCompat.bottom)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
binding.sortModeRecycler.adapter = modeAdapter
|
||||||
|
binding.sortDirectionGroup.addOnButtonCheckedListener(this)
|
||||||
|
binding.sortCancel.setOnClickListener { dismiss() }
|
||||||
|
binding.sortSave.setOnClickListener {
|
||||||
|
applyChosenSort(requireNotNull(getCurrentSort()))
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- STATE SETUP ---
|
||||||
|
modeAdapter.update(getModeChoices(), UpdateInstructions.Diff)
|
||||||
|
|
||||||
|
val initial = getInitialSort()
|
||||||
|
if (initial != null) {
|
||||||
|
modeAdapter.setSelected(initial.mode)
|
||||||
|
val directionId =
|
||||||
|
when (initial.direction) {
|
||||||
|
Sort.Direction.ASCENDING -> R.id.sort_direction_asc
|
||||||
|
Sort.Direction.DESCENDING -> R.id.sort_direction_dsc
|
||||||
|
}
|
||||||
|
binding.sortDirectionGroup.check(directionId)
|
||||||
|
}
|
||||||
|
updateButtons()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyBinding(binding: DialogSortBinding) {
|
||||||
|
super.onDestroyBinding(binding)
|
||||||
|
binding.sortDirectionGroup.removeOnButtonCheckedListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(item: Sort.Mode, viewHolder: RecyclerView.ViewHolder) {
|
||||||
|
modeAdapter.setSelected(item)
|
||||||
|
updateButtons()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onButtonChecked(
|
||||||
|
group: MaterialButtonToggleGroup?,
|
||||||
|
checkedId: Int,
|
||||||
|
isChecked: Boolean
|
||||||
|
) {
|
||||||
|
updateButtons()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateButtons() {
|
||||||
|
val binding = requireBinding()
|
||||||
|
binding.sortSave.isEnabled = getCurrentSort() != getInitialSort()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCurrentSort(): Sort? {
|
||||||
|
val initial = getInitialSort()
|
||||||
|
val mode = modeAdapter.currentMode ?: initial?.mode ?: return null
|
||||||
|
val direction =
|
||||||
|
when (requireBinding().sortDirectionGroup.checkedButtonId) {
|
||||||
|
R.id.sort_direction_asc -> Sort.Direction.ASCENDING
|
||||||
|
R.id.sort_direction_dsc -> Sort.Direction.DESCENDING
|
||||||
|
else -> initial?.direction ?: return null
|
||||||
|
}
|
||||||
|
return Sort(mode, direction)
|
||||||
|
}
|
||||||
|
}
|
||||||
120
app/src/main/java/org/oxycblt/auxio/list/sort/SortModeAdapter.kt
Normal file
120
app/src/main/java/org/oxycblt/auxio/list/sort/SortModeAdapter.kt
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* SortModeAdapter.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.list.sort
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import org.oxycblt.auxio.databinding.ItemSortModeBinding
|
||||||
|
import org.oxycblt.auxio.list.ClickableListListener
|
||||||
|
import org.oxycblt.auxio.list.adapter.FlexibleListAdapter
|
||||||
|
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
|
||||||
|
import org.oxycblt.auxio.util.context
|
||||||
|
import org.oxycblt.auxio.util.inflater
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [FlexibleListAdapter] that displays a list of [Sort.Mode]s.
|
||||||
|
*
|
||||||
|
* @param listener A [ClickableListListener] to bind interactions to.
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
class SortModeAdapter(private val listener: ClickableListListener<Sort.Mode>) :
|
||||||
|
FlexibleListAdapter<Sort.Mode, SortModeViewHolder>(SortModeViewHolder.DIFF_CALLBACK) {
|
||||||
|
/** The currently selected [Sort.Mode] item in this adapter. */
|
||||||
|
var currentMode: Sort.Mode? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
|
SortModeViewHolder.from(parent)
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: SortModeViewHolder, position: Int) {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: SortModeViewHolder, position: Int, payload: List<Any>) {
|
||||||
|
val mode = getItem(position)
|
||||||
|
if (payload.isEmpty()) {
|
||||||
|
holder.bind(mode, listener)
|
||||||
|
}
|
||||||
|
holder.setSelected(mode == currentMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a new [Sort.Mode] option, unselecting the prior one. Does nothing if [mode] equals
|
||||||
|
* [currentMode].
|
||||||
|
*
|
||||||
|
* @param mode The new [Sort.Mode] to select. Should be in the adapter data.
|
||||||
|
*/
|
||||||
|
fun setSelected(mode: Sort.Mode) {
|
||||||
|
if (mode == currentMode) return
|
||||||
|
val oldMode = currentList.indexOf(currentMode)
|
||||||
|
val newMode = currentList.indexOf(mode)
|
||||||
|
currentMode = mode
|
||||||
|
if (oldMode > -1) {
|
||||||
|
notifyItemChanged(oldMode, PAYLOAD_SELECTION_CHANGED)
|
||||||
|
}
|
||||||
|
notifyItemChanged(newMode, PAYLOAD_SELECTION_CHANGED)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val PAYLOAD_SELECTION_CHANGED = Any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [DialogRecyclerView.ViewHolder] that displays a [Sort.Mode].
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
class SortModeViewHolder private constructor(private val binding: ItemSortModeBinding) :
|
||||||
|
DialogRecyclerView.ViewHolder(binding.root) {
|
||||||
|
/**
|
||||||
|
* Bind new data to this instance.
|
||||||
|
*
|
||||||
|
* @param mode The new [Sort.Mode] to bind.
|
||||||
|
* @param listener A [ClickableListListener] to bind interactions to.
|
||||||
|
*/
|
||||||
|
fun bind(mode: Sort.Mode, listener: ClickableListListener<Sort.Mode>) {
|
||||||
|
listener.bind(mode, this)
|
||||||
|
binding.sortRadio.text = binding.context.getString(mode.stringRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set if this view should be shown as selected or not.
|
||||||
|
*
|
||||||
|
* @param selected True if selected, false if not.
|
||||||
|
*/
|
||||||
|
fun setSelected(selected: Boolean) {
|
||||||
|
binding.sortRadio.isChecked = selected
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(parent: View) =
|
||||||
|
SortModeViewHolder(ItemSortModeBinding.inflate(parent.context.inflater))
|
||||||
|
|
||||||
|
val DIFF_CALLBACK =
|
||||||
|
object : DiffUtil.ItemCallback<Sort.Mode>() {
|
||||||
|
override fun areItemsTheSame(oldItem: Sort.Mode, newItem: Sort.Mode) =
|
||||||
|
oldItem == newItem
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: Sort.Mode, newItem: Sort.Mode) =
|
||||||
|
oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -57,7 +57,7 @@ sealed interface IndexingState {
|
||||||
*/
|
*/
|
||||||
sealed interface IndexingProgress {
|
sealed interface IndexingProgress {
|
||||||
/** Other work is being done that does not have a defined progress. */
|
/** Other work is being done that does not have a defined progress. */
|
||||||
object Indeterminate : IndexingProgress
|
data object Indeterminate : IndexingProgress
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Songs are currently being loaded.
|
* Songs are currently being loaded.
|
||||||
|
|
|
||||||
|
|
@ -80,23 +80,23 @@ sealed interface Music : Item {
|
||||||
class UID
|
class UID
|
||||||
private constructor(
|
private constructor(
|
||||||
private val format: Format,
|
private val format: Format,
|
||||||
private val mode: MusicMode,
|
private val type: MusicType,
|
||||||
private val uuid: UUID
|
private val uuid: UUID
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
// Cache the hashCode for HashMap efficiency.
|
// Cache the hashCode for HashMap efficiency.
|
||||||
@IgnoredOnParcel private var hashCode = format.hashCode()
|
@IgnoredOnParcel private var hashCode = format.hashCode()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
hashCode = 31 * hashCode + mode.hashCode()
|
hashCode = 31 * hashCode + type.hashCode()
|
||||||
hashCode = 31 * hashCode + uuid.hashCode()
|
hashCode = 31 * hashCode + uuid.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode() = hashCode
|
override fun hashCode() = hashCode
|
||||||
|
|
||||||
override fun equals(other: Any?) =
|
override fun equals(other: Any?) =
|
||||||
other is UID && format == other.format && mode == other.mode && uuid == other.uuid
|
other is UID && format == other.format && type == other.type && uuid == other.uuid
|
||||||
|
|
||||||
override fun toString() = "${format.namespace}:${mode.intCode.toString(16)}-$uuid"
|
override fun toString() = "${format.namespace}:${type.intCode.toString(16)}-$uuid"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal marker of [Music.UID] format type.
|
* Internal marker of [Music.UID] format type.
|
||||||
|
|
@ -124,23 +124,23 @@ sealed interface Music : Item {
|
||||||
* Creates an Auxio-style [UID] of random composition. Used if there is no
|
* Creates an Auxio-style [UID] of random composition. Used if there is no
|
||||||
* non-subjective, unlikely-to-change metadata of the music.
|
* non-subjective, unlikely-to-change metadata of the music.
|
||||||
*
|
*
|
||||||
* @param mode The analogous [MusicMode] of the item that created this [UID].
|
* @param type The analogous [MusicType] of the item that created this [UID].
|
||||||
*/
|
*/
|
||||||
fun auxio(mode: MusicMode): UID {
|
fun auxio(type: MusicType): UID {
|
||||||
return UID(Format.AUXIO, mode, UUID.randomUUID())
|
return UID(Format.AUXIO, type, UUID.randomUUID())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an Auxio-style [UID] with a [UUID] composed of a hash of the non-subjective,
|
* Creates an Auxio-style [UID] with a [UUID] composed of a hash of the non-subjective,
|
||||||
* unlikely-to-change metadata of the music.
|
* unlikely-to-change metadata of the music.
|
||||||
*
|
*
|
||||||
* @param mode The analogous [MusicMode] of the item that created this [UID].
|
* @param type The analogous [MusicType] of the item that created this [UID].
|
||||||
* @param updates Block to update the [MessageDigest] hash with the metadata of the
|
* @param updates Block to update the [MessageDigest] hash with the metadata of the
|
||||||
* item. Make sure the metadata hashed semantically aligns with the format
|
* item. Make sure the metadata hashed semantically aligns with the format
|
||||||
* specification.
|
* specification.
|
||||||
* @return A new auxio-style [UID].
|
* @return A new auxio-style [UID].
|
||||||
*/
|
*/
|
||||||
fun auxio(mode: MusicMode, updates: MessageDigest.() -> Unit): UID {
|
fun auxio(type: MusicType, updates: MessageDigest.() -> Unit): UID {
|
||||||
val digest =
|
val digest =
|
||||||
MessageDigest.getInstance("SHA-256").run {
|
MessageDigest.getInstance("SHA-256").run {
|
||||||
updates()
|
updates()
|
||||||
|
|
@ -170,19 +170,19 @@ sealed interface Music : Item {
|
||||||
.or(digest[13].toLong().and(0xFF).shl(16))
|
.or(digest[13].toLong().and(0xFF).shl(16))
|
||||||
.or(digest[14].toLong().and(0xFF).shl(8))
|
.or(digest[14].toLong().and(0xFF).shl(8))
|
||||||
.or(digest[15].toLong().and(0xFF)))
|
.or(digest[15].toLong().and(0xFF)))
|
||||||
return UID(Format.AUXIO, mode, uuid)
|
return UID(Format.AUXIO, type, uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a MusicBrainz-style [UID] with a [UUID] derived from the MusicBrainz ID
|
* Creates a MusicBrainz-style [UID] with a [UUID] derived from the MusicBrainz ID
|
||||||
* extracted from a file.
|
* extracted from a file.
|
||||||
*
|
*
|
||||||
* @param mode The analogous [MusicMode] of the item that created this [UID].
|
* @param type The analogous [MusicType] of the item that created this [UID].
|
||||||
* @param mbid The analogous MusicBrainz ID for this item that was extracted from a
|
* @param mbid The analogous MusicBrainz ID for this item that was extracted from a
|
||||||
* file.
|
* file.
|
||||||
* @return A new MusicBrainz-style [UID].
|
* @return A new MusicBrainz-style [UID].
|
||||||
*/
|
*/
|
||||||
fun musicBrainz(mode: MusicMode, mbid: UUID) = UID(Format.MUSICBRAINZ, mode, mbid)
|
fun musicBrainz(type: MusicType, mbid: UUID) = UID(Format.MUSICBRAINZ, type, mbid)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a [UID]'s string representation back into a concrete [UID] instance.
|
* Convert a [UID]'s string representation back into a concrete [UID] instance.
|
||||||
|
|
@ -210,10 +210,10 @@ sealed interface Music : Item {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val mode =
|
val type =
|
||||||
MusicMode.fromIntCode(ids[0].toIntOrNull(16) ?: return null) ?: return null
|
MusicType.fromIntCode(ids[0].toIntOrNull(16) ?: return null) ?: return null
|
||||||
val uuid = ids[1].toUuidOrNull() ?: return null
|
val uuid = ids[1].toUuidOrNull() ?: return null
|
||||||
return UID(format, mode, uuid)
|
return UID(format, type, uuid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,5 +28,6 @@ import javax.inject.Singleton
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface MusicModule {
|
interface MusicModule {
|
||||||
@Singleton @Binds fun repository(musicRepository: MusicRepositoryImpl): MusicRepository
|
@Singleton @Binds fun repository(musicRepository: MusicRepositoryImpl): MusicRepository
|
||||||
|
|
||||||
@Binds fun settings(musicSettingsImpl: MusicSettingsImpl): MusicSettings
|
@Binds fun settings(musicSettingsImpl: MusicSettingsImpl): MusicSettings
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import androidx.core.content.edit
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.list.Sort
|
|
||||||
import org.oxycblt.auxio.music.fs.Directory
|
import org.oxycblt.auxio.music.fs.Directory
|
||||||
import org.oxycblt.auxio.music.fs.MusicDirectories
|
import org.oxycblt.auxio.music.fs.MusicDirectories
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
|
|
@ -47,23 +46,6 @@ interface MusicSettings : Settings<MusicSettings.Listener> {
|
||||||
var multiValueSeparators: String
|
var multiValueSeparators: String
|
||||||
/** Whether to enable more advanced sorting by articles and numbers. */
|
/** Whether to enable more advanced sorting by articles and numbers. */
|
||||||
val intelligentSorting: Boolean
|
val intelligentSorting: Boolean
|
||||||
// TODO: Move sort settings to list module
|
|
||||||
/** The [Sort] mode used in [Song] lists. */
|
|
||||||
var songSort: Sort
|
|
||||||
/** The [Sort] mode used in [Album] lists. */
|
|
||||||
var albumSort: Sort
|
|
||||||
/** The [Sort] mode used in [Artist] lists. */
|
|
||||||
var artistSort: Sort
|
|
||||||
/** The [Sort] mode used in [Genre] lists. */
|
|
||||||
var genreSort: Sort
|
|
||||||
/** The [Sort] mode used in [Playlist] lists. */
|
|
||||||
var playlistSort: Sort
|
|
||||||
/** The [Sort] mode used in an [Album]'s [Song] list. */
|
|
||||||
var albumSongSort: Sort
|
|
||||||
/** The [Sort] mode used in an [Artist]'s [Song] list. */
|
|
||||||
var artistSongSort: Sort
|
|
||||||
/** The [Sort] mode used in a [Genre]'s [Song] list. */
|
|
||||||
var genreSongSort: Sort
|
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
/** Called when a setting controlling how music is loaded has changed. */
|
/** Called when a setting controlling how music is loaded has changed. */
|
||||||
|
|
@ -117,113 +99,6 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context
|
||||||
override val intelligentSorting: Boolean
|
override val intelligentSorting: Boolean
|
||||||
get() = sharedPreferences.getBoolean(getString(R.string.set_key_auto_sort_names), true)
|
get() = sharedPreferences.getBoolean(getString(R.string.set_key_auto_sort_names), true)
|
||||||
|
|
||||||
override var songSort: Sort
|
|
||||||
get() =
|
|
||||||
Sort.fromIntCode(
|
|
||||||
sharedPreferences.getInt(getString(R.string.set_key_songs_sort), Int.MIN_VALUE))
|
|
||||||
?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
|
|
||||||
set(value) {
|
|
||||||
sharedPreferences.edit {
|
|
||||||
putInt(getString(R.string.set_key_songs_sort), value.intCode)
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var albumSort: Sort
|
|
||||||
get() =
|
|
||||||
Sort.fromIntCode(
|
|
||||||
sharedPreferences.getInt(getString(R.string.set_key_albums_sort), Int.MIN_VALUE))
|
|
||||||
?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
|
|
||||||
set(value) {
|
|
||||||
sharedPreferences.edit {
|
|
||||||
putInt(getString(R.string.set_key_albums_sort), value.intCode)
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var artistSort: Sort
|
|
||||||
get() =
|
|
||||||
Sort.fromIntCode(
|
|
||||||
sharedPreferences.getInt(getString(R.string.set_key_artists_sort), Int.MIN_VALUE))
|
|
||||||
?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
|
|
||||||
set(value) {
|
|
||||||
sharedPreferences.edit {
|
|
||||||
putInt(getString(R.string.set_key_artists_sort), value.intCode)
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var genreSort: Sort
|
|
||||||
get() =
|
|
||||||
Sort.fromIntCode(
|
|
||||||
sharedPreferences.getInt(getString(R.string.set_key_genres_sort), Int.MIN_VALUE))
|
|
||||||
?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
|
|
||||||
set(value) {
|
|
||||||
sharedPreferences.edit {
|
|
||||||
putInt(getString(R.string.set_key_genres_sort), value.intCode)
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var playlistSort: Sort
|
|
||||||
get() =
|
|
||||||
Sort.fromIntCode(
|
|
||||||
sharedPreferences.getInt(getString(R.string.set_key_playlists_sort), Int.MIN_VALUE))
|
|
||||||
?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
|
|
||||||
set(value) {
|
|
||||||
sharedPreferences.edit {
|
|
||||||
putInt(getString(R.string.set_key_playlists_sort), value.intCode)
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override var albumSongSort: Sort
|
|
||||||
get() {
|
|
||||||
var sort =
|
|
||||||
Sort.fromIntCode(
|
|
||||||
sharedPreferences.getInt(
|
|
||||||
getString(R.string.set_key_album_songs_sort), Int.MIN_VALUE))
|
|
||||||
?: Sort(Sort.Mode.ByDisc, Sort.Direction.ASCENDING)
|
|
||||||
|
|
||||||
// Correct legacy album sort modes to Disc
|
|
||||||
if (sort.mode is Sort.Mode.ByName) {
|
|
||||||
sort = sort.withMode(Sort.Mode.ByDisc)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sort
|
|
||||||
}
|
|
||||||
set(value) {
|
|
||||||
sharedPreferences.edit {
|
|
||||||
putInt(getString(R.string.set_key_album_songs_sort), value.intCode)
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var artistSongSort: Sort
|
|
||||||
get() =
|
|
||||||
Sort.fromIntCode(
|
|
||||||
sharedPreferences.getInt(
|
|
||||||
getString(R.string.set_key_artist_songs_sort), Int.MIN_VALUE))
|
|
||||||
?: Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING)
|
|
||||||
set(value) {
|
|
||||||
sharedPreferences.edit {
|
|
||||||
putInt(getString(R.string.set_key_artist_songs_sort), value.intCode)
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var genreSongSort: Sort
|
|
||||||
get() =
|
|
||||||
Sort.fromIntCode(
|
|
||||||
sharedPreferences.getInt(
|
|
||||||
getString(R.string.set_key_genre_songs_sort), Int.MIN_VALUE))
|
|
||||||
?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
|
|
||||||
set(value) {
|
|
||||||
sharedPreferences.edit {
|
|
||||||
putInt(getString(R.string.set_key_genre_songs_sort), value.intCode)
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSettingChanged(key: String, listener: MusicSettings.Listener) {
|
override fun onSettingChanged(key: String, listener: MusicSettings.Listener) {
|
||||||
// TODO: Differentiate "hard reloads" (Need the cache) and "Soft reloads"
|
// TODO: Differentiate "hard reloads" (Need the cache) and "Soft reloads"
|
||||||
// (just need to manipulate data)
|
// (just need to manipulate data)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2022 Auxio Project
|
* Copyright (c) 2022 Auxio Project
|
||||||
* MusicMode.kt is part of Auxio.
|
* MusicType.kt is part of Auxio.
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -21,20 +21,20 @@ package org.oxycblt.auxio.music
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a data configuration corresponding to a specific type of [Music],
|
* General configuration enum to control what kind of music is being worked with.
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
enum class MusicMode {
|
enum class MusicType {
|
||||||
/** Configure with respect to [Song] instances. */
|
/** @see Song */
|
||||||
SONGS,
|
SONGS,
|
||||||
/** Configure with respect to [Album] instances. */
|
/** @see Album */
|
||||||
ALBUMS,
|
ALBUMS,
|
||||||
/** Configure with respect to [Artist] instances. */
|
/** @see Artist */
|
||||||
ARTISTS,
|
ARTISTS,
|
||||||
/** Configure with respect to [Genre] instances. */
|
/** @see Genre */
|
||||||
GENRES,
|
GENRES,
|
||||||
/** Configure with respect to [Playlist] instances. */
|
/** @see Playlist */
|
||||||
PLAYLISTS;
|
PLAYLISTS;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -54,11 +54,11 @@ enum class MusicMode {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Convert a [MusicMode] integer representation into an instance.
|
* Convert a [MusicType] integer representation into an instance.
|
||||||
*
|
*
|
||||||
* @param intCode An integer representation of a [MusicMode]
|
* @param intCode An integer representation of a [MusicType]
|
||||||
* @return The corresponding [MusicMode], or null if the [MusicMode] is invalid.
|
* @return The corresponding [MusicType], or null if the [MusicType] is invalid.
|
||||||
* @see MusicMode.intCode
|
* @see MusicType.intCode
|
||||||
*/
|
*/
|
||||||
fun fromIntCode(intCode: Int) =
|
fun fromIntCode(intCode: Int) =
|
||||||
when (intCode) {
|
when (intCode) {
|
||||||
|
|
@ -26,6 +26,7 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.oxycblt.auxio.list.ListSettings
|
||||||
import org.oxycblt.auxio.util.Event
|
import org.oxycblt.auxio.util.Event
|
||||||
import org.oxycblt.auxio.util.MutableEvent
|
import org.oxycblt.auxio.util.MutableEvent
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
@ -39,8 +40,8 @@ import org.oxycblt.auxio.util.logD
|
||||||
class MusicViewModel
|
class MusicViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
|
private val listSettings: ListSettings,
|
||||||
private val musicRepository: MusicRepository,
|
private val musicRepository: MusicRepository,
|
||||||
private val musicSettings: MusicSettings
|
|
||||||
) : ViewModel(), MusicRepository.UpdateListener, MusicRepository.IndexingListener {
|
) : ViewModel(), MusicRepository.UpdateListener, MusicRepository.IndexingListener {
|
||||||
|
|
||||||
private val _indexingState = MutableStateFlow<IndexingState?>(null)
|
private val _indexingState = MutableStateFlow<IndexingState?>(null)
|
||||||
|
|
@ -167,7 +168,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun addToPlaylist(album: Album, playlist: Playlist? = null) {
|
fun addToPlaylist(album: Album, playlist: Playlist? = null) {
|
||||||
logD("Adding $album to playlist")
|
logD("Adding $album to playlist")
|
||||||
addToPlaylist(musicSettings.albumSongSort.songs(album.songs), playlist)
|
addToPlaylist(listSettings.albumSongSort.songs(album.songs), playlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -178,7 +179,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun addToPlaylist(artist: Artist, playlist: Playlist? = null) {
|
fun addToPlaylist(artist: Artist, playlist: Playlist? = null) {
|
||||||
logD("Adding $artist to playlist")
|
logD("Adding $artist to playlist")
|
||||||
addToPlaylist(musicSettings.artistSongSort.songs(artist.songs), playlist)
|
addToPlaylist(listSettings.artistSongSort.songs(artist.songs), playlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -189,7 +190,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun addToPlaylist(genre: Genre, playlist: Playlist? = null) {
|
fun addToPlaylist(genre: Genre, playlist: Playlist? = null) {
|
||||||
logD("Adding $genre to playlist")
|
logD("Adding $genre to playlist")
|
||||||
addToPlaylist(musicSettings.genreSongSort.songs(genre.songs), playlist)
|
addToPlaylist(listSettings.genreSongSort.songs(genre.songs), playlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,9 @@ abstract class CacheDatabase : RoomDatabase() {
|
||||||
@Dao
|
@Dao
|
||||||
interface CachedSongsDao {
|
interface CachedSongsDao {
|
||||||
@Query("SELECT * FROM CachedSong") suspend fun readSongs(): List<CachedSong>
|
@Query("SELECT * FROM CachedSong") suspend fun readSongs(): List<CachedSong>
|
||||||
|
|
||||||
@Query("DELETE FROM CachedSong") suspend fun nukeSongs()
|
@Query("DELETE FROM CachedSong") suspend fun nukeSongs()
|
||||||
|
|
||||||
@Insert suspend fun insertSongs(songs: List<CachedSong>)
|
@Insert suspend fun insertSongs(songs: List<CachedSong>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ private class CacheImpl(cachedSongs: List<CachedSong>) : Cache {
|
||||||
}
|
}
|
||||||
|
|
||||||
override var invalidated = false
|
override var invalidated = false
|
||||||
|
|
||||||
override fun populate(rawSong: RawSong): Boolean {
|
override fun populate(rawSong: RawSong): Boolean {
|
||||||
// For a cached raw song to be used, it must exist within the cache and have matching
|
// For a cached raw song to be used, it must exist within the cache and have matching
|
||||||
// addition and modification timestamps. Technically the addition timestamp doesn't
|
// addition and modification timestamps. Technically the addition timestamp doesn't
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
|
|
@ -260,9 +260,9 @@ sealed interface ChosenName {
|
||||||
/** The current name already exists. */
|
/** The current name already exists. */
|
||||||
data class AlreadyExists(val prior: String) : ChosenName
|
data class AlreadyExists(val prior: String) : ChosenName
|
||||||
/** The current name is empty. */
|
/** The current name is empty. */
|
||||||
object Empty : ChosenName
|
data object Empty : ChosenName
|
||||||
/** The current name only consists of whitespace. */
|
/** The current name only consists of whitespace. */
|
||||||
object Blank : ChosenName
|
data object Blank : ChosenName
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ interface DeviceLibrary {
|
||||||
* Find a [Album] instance corresponding to the given [Music.UID].
|
* Find a [Album] instance corresponding to the given [Music.UID].
|
||||||
*
|
*
|
||||||
* @param uid The [Music.UID] to search for.
|
* @param uid The [Music.UID] to search for.
|
||||||
* @return The corresponding [Song], or null if one was not found.
|
* @return The corresponding [Album], or null if one was not found.
|
||||||
*/
|
*/
|
||||||
fun findAlbum(uid: Music.UID): Album?
|
fun findAlbum(uid: Music.UID): Album?
|
||||||
|
|
||||||
|
|
@ -83,7 +83,7 @@ interface DeviceLibrary {
|
||||||
* Find a [Artist] instance corresponding to the given [Music.UID].
|
* Find a [Artist] instance corresponding to the given [Music.UID].
|
||||||
*
|
*
|
||||||
* @param uid The [Music.UID] to search for.
|
* @param uid The [Music.UID] to search for.
|
||||||
* @return The corresponding [Song], or null if one was not found.
|
* @return The corresponding [Artist], or null if one was not found.
|
||||||
*/
|
*/
|
||||||
fun findArtist(uid: Music.UID): Artist?
|
fun findArtist(uid: Music.UID): Artist?
|
||||||
|
|
||||||
|
|
@ -91,7 +91,7 @@ interface DeviceLibrary {
|
||||||
* Find a [Genre] instance corresponding to the given [Music.UID].
|
* Find a [Genre] instance corresponding to the given [Music.UID].
|
||||||
*
|
*
|
||||||
* @param uid The [Music.UID] to search for.
|
* @param uid The [Music.UID] to search for.
|
||||||
* @return The corresponding [Song], or null if one was not found.
|
* @return The corresponding [Genre], or null if one was not found.
|
||||||
*/
|
*/
|
||||||
fun findGenre(uid: Music.UID): Genre?
|
fun findGenre(uid: Music.UID): Genre?
|
||||||
|
|
||||||
|
|
@ -266,14 +266,19 @@ class DeviceLibraryImpl(
|
||||||
|
|
||||||
// All other music is built from songs, so comparison only needs to check songs.
|
// All other music is built from songs, so comparison only needs to check songs.
|
||||||
override fun equals(other: Any?) = other is DeviceLibrary && other.songs == songs
|
override fun equals(other: Any?) = other is DeviceLibrary && other.songs == songs
|
||||||
|
|
||||||
override fun hashCode() = songs.hashCode()
|
override fun hashCode() = songs.hashCode()
|
||||||
|
|
||||||
override fun toString() =
|
override fun toString() =
|
||||||
"DeviceLibrary(songs=${songs.size}, albums=${albums.size}, " +
|
"DeviceLibrary(songs=${songs.size}, albums=${albums.size}, " +
|
||||||
"artists=${artists.size}, genres=${genres.size})"
|
"artists=${artists.size}, genres=${genres.size})"
|
||||||
|
|
||||||
override fun findSong(uid: Music.UID): Song? = songUidMap[uid]
|
override fun findSong(uid: Music.UID): Song? = songUidMap[uid]
|
||||||
|
|
||||||
override fun findAlbum(uid: Music.UID): Album? = albumUidMap[uid]
|
override fun findAlbum(uid: Music.UID): Album? = albumUidMap[uid]
|
||||||
|
|
||||||
override fun findArtist(uid: Music.UID): Artist? = artistUidMap[uid]
|
override fun findArtist(uid: Music.UID): Artist? = artistUidMap[uid]
|
||||||
|
|
||||||
override fun findGenre(uid: Music.UID): Genre? = genreUidMap[uid]
|
override fun findGenre(uid: Music.UID): Genre? = genreUidMap[uid]
|
||||||
|
|
||||||
override fun findSongForUri(context: Context, uri: Uri) =
|
override fun findSongForUri(context: Context, uri: Uri) =
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,13 @@ package org.oxycblt.auxio.music.device
|
||||||
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.image.extractor.CoverUri
|
import org.oxycblt.auxio.image.extractor.CoverUri
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
|
import org.oxycblt.auxio.music.MusicType
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.fs.MimeType
|
import org.oxycblt.auxio.music.fs.MimeType
|
||||||
import org.oxycblt.auxio.music.fs.Path
|
import org.oxycblt.auxio.music.fs.Path
|
||||||
|
|
@ -54,8 +54,8 @@ import org.oxycblt.auxio.util.update
|
||||||
class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Song {
|
class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Song {
|
||||||
override val uid =
|
override val uid =
|
||||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
||||||
rawSong.musicBrainzId?.toUuidOrNull()?.let { Music.UID.musicBrainz(MusicMode.SONGS, it) }
|
rawSong.musicBrainzId?.toUuidOrNull()?.let { Music.UID.musicBrainz(MusicType.SONGS, it) }
|
||||||
?: Music.UID.auxio(MusicMode.SONGS) {
|
?: Music.UID.auxio(MusicType.SONGS) {
|
||||||
// Song UIDs are based on the raw data without parsing so that they remain
|
// Song UIDs are based on the raw data without parsing so that they remain
|
||||||
// consistent across music setting changes. Parents are not held up to the
|
// consistent across music setting changes. Parents are not held up to the
|
||||||
// same standard since grouping is already inherently linked to settings.
|
// same standard since grouping is already inherently linked to settings.
|
||||||
|
|
@ -102,8 +102,10 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
|
||||||
private val hashCode = 31 * uid.hashCode() + rawSong.hashCode()
|
private val hashCode = 31 * uid.hashCode() + rawSong.hashCode()
|
||||||
|
|
||||||
override fun hashCode() = hashCode
|
override fun hashCode() = hashCode
|
||||||
|
|
||||||
override fun equals(other: Any?) =
|
override fun equals(other: Any?) =
|
||||||
other is SongImpl && uid == other.uid && rawSong == other.rawSong
|
other is SongImpl && uid == other.uid && rawSong == other.rawSong
|
||||||
|
|
||||||
override fun toString() = "Song(uid=$uid, name=$name)"
|
override fun toString() = "Song(uid=$uid, name=$name)"
|
||||||
|
|
||||||
private val artistMusicBrainzIds = rawSong.artistMusicBrainzIds.parseMultiValue(musicSettings)
|
private val artistMusicBrainzIds = rawSong.artistMusicBrainzIds.parseMultiValue(musicSettings)
|
||||||
|
|
@ -251,8 +253,8 @@ class AlbumImpl(
|
||||||
|
|
||||||
override val uid =
|
override val uid =
|
||||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
||||||
rawAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ALBUMS, it) }
|
rawAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicType.ALBUMS, it) }
|
||||||
?: Music.UID.auxio(MusicMode.ALBUMS) {
|
?: Music.UID.auxio(MusicType.ALBUMS) {
|
||||||
// Hash based on only names despite the presence of a date to increase stability.
|
// Hash based on only names despite the presence of a date to increase stability.
|
||||||
// I don't know if there is any situation where an artist will have two albums with
|
// I don't know if there is any situation where an artist will have two albums with
|
||||||
// the exact same name, but if there is, I would love to know.
|
// the exact same name, but if there is, I would love to know.
|
||||||
|
|
@ -313,8 +315,10 @@ class AlbumImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode() = hashCode
|
override fun hashCode() = hashCode
|
||||||
|
|
||||||
override fun equals(other: Any?) =
|
override fun equals(other: Any?) =
|
||||||
other is AlbumImpl && uid == other.uid && rawAlbum == other.rawAlbum && songs == other.songs
|
other is AlbumImpl && uid == other.uid && rawAlbum == other.rawAlbum && songs == other.songs
|
||||||
|
|
||||||
override fun toString() = "Album(uid=$uid, name=$name)"
|
override fun toString() = "Album(uid=$uid, name=$name)"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -366,8 +370,8 @@ class ArtistImpl(grouping: Grouping<RawArtist, Music>, musicSettings: MusicSetti
|
||||||
|
|
||||||
override val uid =
|
override val uid =
|
||||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
||||||
rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) }
|
rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicType.ARTISTS, it) }
|
||||||
?: Music.UID.auxio(MusicMode.ARTISTS) { update(rawArtist.name) }
|
?: Music.UID.auxio(MusicType.ARTISTS) { update(rawArtist.name) }
|
||||||
override val name =
|
override val name =
|
||||||
rawArtist.name?.let { Name.Known.from(it, rawArtist.sortName, musicSettings) }
|
rawArtist.name?.let { Name.Known.from(it, rawArtist.sortName, musicSettings) }
|
||||||
?: Name.Unknown(R.string.def_artist)
|
?: Name.Unknown(R.string.def_artist)
|
||||||
|
|
@ -461,7 +465,7 @@ class ArtistImpl(grouping: Grouping<RawArtist, Music>, musicSettings: MusicSetti
|
||||||
class GenreImpl(grouping: Grouping<RawGenre, SongImpl>, musicSettings: MusicSettings) : Genre {
|
class GenreImpl(grouping: Grouping<RawGenre, SongImpl>, musicSettings: MusicSettings) : Genre {
|
||||||
private val rawGenre = grouping.raw.inner
|
private val rawGenre = grouping.raw.inner
|
||||||
|
|
||||||
override val uid = Music.UID.auxio(MusicMode.GENRES) { update(rawGenre.name) }
|
override val uid = Music.UID.auxio(MusicType.GENRES) { update(rawGenre.name) }
|
||||||
override val name =
|
override val name =
|
||||||
rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) }
|
rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) }
|
||||||
?: Name.Unknown(R.string.def_genre)
|
?: Name.Unknown(R.string.def_genre)
|
||||||
|
|
|
||||||
|
|
@ -74,9 +74,13 @@ interface MediaStoreExtractor {
|
||||||
/** A black-box interface representing a query from the media database. */
|
/** A black-box interface representing a query from the media database. */
|
||||||
interface Query {
|
interface Query {
|
||||||
val projectedTotal: Int
|
val projectedTotal: Int
|
||||||
|
|
||||||
fun moveToNext(): Boolean
|
fun moveToNext(): Boolean
|
||||||
|
|
||||||
fun close()
|
fun close()
|
||||||
|
|
||||||
fun populateFileInfo(rawSong: RawSong)
|
fun populateFileInfo(rawSong: RawSong)
|
||||||
|
|
||||||
fun populateTags(rawSong: RawSong)
|
fun populateTags(rawSong: RawSong)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -285,7 +289,9 @@ private abstract class BaseMediaStoreExtractor(
|
||||||
private val albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST)
|
private val albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST)
|
||||||
|
|
||||||
final override val projectedTotal = cursor.count
|
final override val projectedTotal = cursor.count
|
||||||
|
|
||||||
final override fun moveToNext() = cursor.moveToNext()
|
final override fun moveToNext() = cursor.moveToNext()
|
||||||
|
|
||||||
final override fun close() = cursor.close()
|
final override fun close() = cursor.close()
|
||||||
|
|
||||||
override fun populateFileInfo(rawSong: RawSong) {
|
override fun populateFileInfo(rawSong: RawSong) {
|
||||||
|
|
@ -524,6 +530,7 @@ private class Api29MediaStoreExtractor(context: Context, musicSettings: MusicSet
|
||||||
storageManager: StorageManager
|
storageManager: StorageManager
|
||||||
) : BaseApi29MediaStoreExtractor.Query(cursor, genreNamesMap, storageManager) {
|
) : BaseApi29MediaStoreExtractor.Query(cursor, genreNamesMap, storageManager) {
|
||||||
private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
||||||
|
|
||||||
override fun populateTags(rawSong: RawSong) {
|
override fun populateTags(rawSong: RawSong) {
|
||||||
super.populateTags(rawSong)
|
super.populateTags(rawSong)
|
||||||
// This extractor is volume-aware, but does not support the modern track columns.
|
// This extractor is volume-aware, but does not support the modern track columns.
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ inline fun <reified R> ContentResolver.useQuery(
|
||||||
) = safeQuery(uri, projection, selector, args).use(block)
|
) = safeQuery(uri, projection, selector, args).use(block)
|
||||||
|
|
||||||
/** Album art [MediaStore] database is not a built-in constant, have to define it ourselves. */
|
/** Album art [MediaStore] database is not a built-in constant, have to define it ourselves. */
|
||||||
private val EXTERNAL_COVERS_URI = Uri.parse("content://media/external/audio/albumart")
|
private val externalCoversUri = Uri.parse("content://media/external/audio/albumart")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a [MediaStore] Song ID into a [Uri] to it's audio file.
|
* Convert a [MediaStore] Song ID into a [Uri] to it's audio file.
|
||||||
|
|
@ -102,21 +102,11 @@ fun Long.toAudioUri() =
|
||||||
* @return An external storage image [Uri]. May not exist.
|
* @return An external storage image [Uri]. May not exist.
|
||||||
* @see ContentUris.withAppendedId
|
* @see ContentUris.withAppendedId
|
||||||
*/
|
*/
|
||||||
fun Long.toCoverUri() = ContentUris.withAppendedId(EXTERNAL_COVERS_URI, this)
|
fun Long.toCoverUri() = ContentUris.withAppendedId(externalCoversUri, this)
|
||||||
|
|
||||||
// --- STORAGEMANAGER UTILITIES ---
|
// --- STORAGEMANAGER UTILITIES ---
|
||||||
// Largely derived from Material Files: https://github.com/zhanghai/MaterialFiles
|
// Largely derived from Material Files: https://github.com/zhanghai/MaterialFiles
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the analogous method to [StorageManager.getStorageVolumes] method that is usable from
|
|
||||||
* API 21 to API 23, in which the [StorageManager] API was hidden and differed greatly.
|
|
||||||
*
|
|
||||||
* @see StorageManager.getStorageVolumes
|
|
||||||
*/
|
|
||||||
@Suppress("NewApi")
|
|
||||||
private val SM_API21_GET_VOLUME_LIST_METHOD: Method by
|
|
||||||
lazyReflectedMethod(StorageManager::class, "getVolumeList")
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides the analogous method to [StorageVolume.getDirectory] method that is usable from API 21
|
* Provides the analogous method to [StorageVolume.getDirectory] method that is usable from API 21
|
||||||
* to API 23, in which the [StorageVolume] API was hidden and differed greatly.
|
* to API 23, in which the [StorageVolume] API was hidden and differed greatly.
|
||||||
|
|
@ -124,7 +114,7 @@ private val SM_API21_GET_VOLUME_LIST_METHOD: Method by
|
||||||
* @see StorageVolume.getDirectory
|
* @see StorageVolume.getDirectory
|
||||||
*/
|
*/
|
||||||
@Suppress("NewApi")
|
@Suppress("NewApi")
|
||||||
private val SV_API21_GET_PATH_METHOD: Method by lazyReflectedMethod(StorageVolume::class, "getPath")
|
private val svApi21GetPathMethod: Method by lazyReflectedMethod(StorageVolume::class, "getPath")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [StorageVolume] considered the "primary" volume by the system, obtained in a
|
* The [StorageVolume] considered the "primary" volume by the system, obtained in a
|
||||||
|
|
@ -143,13 +133,7 @@ val StorageManager.primaryStorageVolumeCompat: StorageVolume
|
||||||
* @see StorageManager.getStorageVolumes
|
* @see StorageManager.getStorageVolumes
|
||||||
*/
|
*/
|
||||||
val StorageManager.storageVolumesCompat: List<StorageVolume>
|
val StorageManager.storageVolumesCompat: List<StorageVolume>
|
||||||
get() =
|
get() = storageVolumes.toList()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
storageVolumes.toList()
|
|
||||||
} else {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
(SM_API21_GET_VOLUME_LIST_METHOD.invoke(this) as Array<StorageVolume>).toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The the absolute path to this [StorageVolume]'s directory within the file-system, in a
|
* The the absolute path to this [StorageVolume]'s directory within the file-system, in a
|
||||||
|
|
@ -166,8 +150,7 @@ val StorageVolume.directoryCompat: String?
|
||||||
// Replicate API: Analogous method if mounted, null if not
|
// Replicate API: Analogous method if mounted, null if not
|
||||||
when (stateCompat) {
|
when (stateCompat) {
|
||||||
Environment.MEDIA_MOUNTED,
|
Environment.MEDIA_MOUNTED,
|
||||||
Environment.MEDIA_MOUNTED_READ_ONLY ->
|
Environment.MEDIA_MOUNTED_READ_ONLY -> svApi21GetPathMethod.invoke(this) as String
|
||||||
SV_API21_GET_PATH_METHOD.invoke(this) as String
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,11 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?) = other is Date && compareTo(other) == 0
|
override fun equals(other: Any?) = other is Date && compareTo(other) == 0
|
||||||
|
|
||||||
override fun hashCode() = tokens.hashCode()
|
override fun hashCode() = tokens.hashCode()
|
||||||
|
|
||||||
override fun toString() = StringBuilder().appendDate().toString()
|
override fun toString() = StringBuilder().appendDate().toString()
|
||||||
|
|
||||||
override fun compareTo(other: Date): Int {
|
override fun compareTo(other: Date): Int {
|
||||||
for (i in 0 until max(tokens.size, other.tokens.size)) {
|
for (i in 0 until max(tokens.size, other.tokens.size)) {
|
||||||
val ai = tokens.getOrNull(i)
|
val ai = tokens.getOrNull(i)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ import org.oxycblt.auxio.list.Item
|
||||||
class Disc(val number: Int, val name: String?) : Item, Comparable<Disc> {
|
class Disc(val number: Int, val name: String?) : Item, Comparable<Disc> {
|
||||||
// We don't want to group discs by differing subtitles, so only compare by the number
|
// We don't want to group discs by differing subtitles, so only compare by the number
|
||||||
override fun equals(other: Any?) = other is Disc && number == other.number
|
override fun equals(other: Any?) = other is Disc && number == other.number
|
||||||
|
|
||||||
override fun hashCode() = number.hashCode()
|
override fun hashCode() = number.hashCode()
|
||||||
|
|
||||||
override fun compareTo(other: Disc) = number.compareTo(other.number)
|
override fun compareTo(other: Disc) = number.compareTo(other.number)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,9 @@ sealed interface Name : Comparable<Name> {
|
||||||
*/
|
*/
|
||||||
data class Unknown(@StringRes val stringRes: Int) : Name {
|
data class Unknown(@StringRes val stringRes: Int) : Name {
|
||||||
override val thumb = "?"
|
override val thumb = "?"
|
||||||
|
|
||||||
override fun resolve(context: Context) = context.getString(stringRes)
|
override fun resolve(context: Context) = context.getString(stringRes)
|
||||||
|
|
||||||
override fun compareTo(other: Name) =
|
override fun compareTo(other: Name) =
|
||||||
when (other) {
|
when (other) {
|
||||||
// Unknown names do not need any direct comparison right now.
|
// Unknown names do not need any direct comparison right now.
|
||||||
|
|
@ -143,8 +145,8 @@ sealed interface Name : Comparable<Name> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val COLLATOR: Collator = Collator.getInstance().apply { strength = Collator.PRIMARY }
|
private val collator: Collator = Collator.getInstance().apply { strength = Collator.PRIMARY }
|
||||||
private val PUNCT_REGEX by lazy { Regex("[\\p{Punct}+]") }
|
private val punctRegex by lazy { Regex("[\\p{Punct}+]") }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plain [Name.Known] implementation that is internationalization-safe.
|
* Plain [Name.Known] implementation that is internationalization-safe.
|
||||||
|
|
@ -157,8 +159,8 @@ private data class SimpleKnownName(override val raw: String, override val sort:
|
||||||
|
|
||||||
private fun parseToken(name: String): SortToken {
|
private fun parseToken(name: String): SortToken {
|
||||||
// Remove excess punctuation from the string, as those usually aren't considered in sorting.
|
// Remove excess punctuation from the string, as those usually aren't considered in sorting.
|
||||||
val stripped = name.replace(PUNCT_REGEX, "").ifEmpty { name }
|
val stripped = name.replace(punctRegex, "").ifEmpty { name }
|
||||||
val collationKey = COLLATOR.getCollationKey(stripped)
|
val collationKey = collator.getCollationKey(stripped)
|
||||||
// Always use lexicographic mode since we aren't parsing any numeric components
|
// Always use lexicographic mode since we aren't parsing any numeric components
|
||||||
return SortToken(collationKey, SortToken.Type.LEXICOGRAPHIC)
|
return SortToken(collationKey, SortToken.Type.LEXICOGRAPHIC)
|
||||||
}
|
}
|
||||||
|
|
@ -179,7 +181,7 @@ private data class IntelligentKnownName(override val raw: String, override val s
|
||||||
val stripped =
|
val stripped =
|
||||||
name
|
name
|
||||||
// Remove excess punctuation from the string, as those u
|
// Remove excess punctuation from the string, as those u
|
||||||
.replace(PUNCT_REGEX, "")
|
.replace(punctRegex, "")
|
||||||
.ifEmpty { name }
|
.ifEmpty { name }
|
||||||
.run {
|
.run {
|
||||||
// Strip any english articles like "the" or "an" from the start, as music
|
// Strip any english articles like "the" or "an" from the start, as music
|
||||||
|
|
@ -206,10 +208,10 @@ private data class IntelligentKnownName(override val raw: String, override val s
|
||||||
val digits =
|
val digits =
|
||||||
token.trimStart { Character.getNumericValue(it) == 0 }.ifEmpty { token }
|
token.trimStart { Character.getNumericValue(it) == 0 }.ifEmpty { token }
|
||||||
// Other languages have other types of digit strings, still use collation keys
|
// Other languages have other types of digit strings, still use collation keys
|
||||||
collationKey = COLLATOR.getCollationKey(digits)
|
collationKey = collator.getCollationKey(digits)
|
||||||
type = SortToken.Type.NUMERIC
|
type = SortToken.Type.NUMERIC
|
||||||
} else {
|
} else {
|
||||||
collationKey = COLLATOR.getCollationKey(token)
|
collationKey = collator.getCollationKey(token)
|
||||||
type = SortToken.Type.LEXICOGRAPHIC
|
type = SortToken.Type.LEXICOGRAPHIC
|
||||||
}
|
}
|
||||||
SortToken(collationKey, type)
|
SortToken(collationKey, type)
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ sealed interface ReleaseType {
|
||||||
* A soundtrack. Similar to a [Compilation], but created for a specific piece of (usually
|
* A soundtrack. Similar to a [Compilation], but created for a specific piece of (usually
|
||||||
* visual) media.
|
* visual) media.
|
||||||
*/
|
*/
|
||||||
object Soundtrack : ReleaseType {
|
data object Soundtrack : ReleaseType {
|
||||||
override val refinement: Refinement?
|
override val refinement: Refinement?
|
||||||
get() = null
|
get() = null
|
||||||
|
|
||||||
|
|
@ -123,7 +123,7 @@ sealed interface ReleaseType {
|
||||||
* A (DJ) Mix. These are usually one large track consisting of the artist playing several
|
* A (DJ) Mix. These are usually one large track consisting of the artist playing several
|
||||||
* sub-tracks with smooth transitions between them.
|
* sub-tracks with smooth transitions between them.
|
||||||
*/
|
*/
|
||||||
object Mix : ReleaseType {
|
data object Mix : ReleaseType {
|
||||||
override val refinement: Refinement?
|
override val refinement: Refinement?
|
||||||
get() = null
|
get() = null
|
||||||
|
|
||||||
|
|
@ -135,7 +135,7 @@ sealed interface ReleaseType {
|
||||||
* A Mix-tape. These are usually [EP]-sized releases of music made to promote an Artist or a
|
* A Mix-tape. These are usually [EP]-sized releases of music made to promote an Artist or a
|
||||||
* future release.
|
* future release.
|
||||||
*/
|
*/
|
||||||
object Mixtape : ReleaseType {
|
data object Mixtape : ReleaseType {
|
||||||
override val refinement: Refinement?
|
override val refinement: Refinement?
|
||||||
get() = null
|
get() = null
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ import dagger.hilt.components.SingletonComponent
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface MetadataModule {
|
interface MetadataModule {
|
||||||
@Binds fun tagExtractor(extractor: TagExtractorImpl): TagExtractor
|
@Binds fun tagExtractor(extractor: TagExtractorImpl): TagExtractor
|
||||||
|
|
||||||
@Binds fun tagWorkerFactory(factory: TagWorkerFactoryImpl): TagWorker.Factory
|
@Binds fun tagWorkerFactory(factory: TagWorkerFactoryImpl): TagWorker.Factory
|
||||||
|
|
||||||
@Binds fun audioPropertiesFactory(factory: AudioPropertiesFactoryImpl): AudioProperties.Factory
|
@Binds fun audioPropertiesFactory(factory: AudioPropertiesFactoryImpl): AudioProperties.Factory
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -204,14 +204,14 @@ private fun String.parseId3v1Genre(): String? {
|
||||||
"RX" -> "Remix"
|
"RX" -> "Remix"
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
return GENRE_TABLE.getOrNull(numeric)
|
return genreTable.getOrNull(numeric)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Regex] that implements parsing for ID3v2's genre format. Derived from mutagen:
|
* A [Regex] that implements parsing for ID3v2's genre format. Derived from mutagen:
|
||||||
* https://github.com/quodlibet/mutagen
|
* https://github.com/quodlibet/mutagen
|
||||||
*/
|
*/
|
||||||
private val ID3V2_GENRE_RE by lazy { Regex("((?:\\((\\d+|RX|CR)\\))*)(.+)?") }
|
private val id3v2GenreRe by lazy { Regex("((?:\\((\\d+|RX|CR)\\))*)(.+)?") }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse an ID3v2 integer genre field, which has support for multiple genre values and combined
|
* Parse an ID3v2 integer genre field, which has support for multiple genre values and combined
|
||||||
|
|
@ -220,7 +220,7 @@ private val ID3V2_GENRE_RE by lazy { Regex("((?:\\((\\d+|RX|CR)\\))*)(.+)?") }
|
||||||
* @return A list of one or more genres, or null if the field is not a valid ID3v2 integer genre.
|
* @return A list of one or more genres, or null if the field is not a valid ID3v2 integer genre.
|
||||||
*/
|
*/
|
||||||
private fun String.parseId3v2Genre(): List<String>? {
|
private fun String.parseId3v2Genre(): List<String>? {
|
||||||
val groups = (ID3V2_GENRE_RE.matchEntire(this) ?: return null).groupValues
|
val groups = (id3v2GenreRe.matchEntire(this) ?: return null).groupValues
|
||||||
val genres = mutableSetOf<String>()
|
val genres = mutableSetOf<String>()
|
||||||
|
|
||||||
// ID3v2.3 genres are far more complex and require string grokking to properly implement.
|
// ID3v2.3 genres are far more complex and require string grokking to properly implement.
|
||||||
|
|
@ -260,7 +260,7 @@ private fun String.parseId3v2Genre(): List<String>? {
|
||||||
* A table of the "conventional" mapping between ID3v1 integer genres and their named counterparts.
|
* A table of the "conventional" mapping between ID3v1 integer genres and their named counterparts.
|
||||||
* Includes non-standard extensions.
|
* Includes non-standard extensions.
|
||||||
*/
|
*/
|
||||||
private val GENRE_TABLE =
|
private val genreTable =
|
||||||
arrayOf(
|
arrayOf(
|
||||||
// ID3 Standard
|
// ID3 Standard
|
||||||
"Blues",
|
"Blues",
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ import org.oxycblt.auxio.util.newMainPendingIntent
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class IndexingNotification(private val context: Context) :
|
class IndexingNotification(private val context: Context) :
|
||||||
ForegroundServiceNotification(context, INDEXER_CHANNEL) {
|
ForegroundServiceNotification(context, indexerChannel) {
|
||||||
private var lastUpdateTime = -1L
|
private var lastUpdateTime = -1L
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
@ -98,7 +98,7 @@ class IndexingNotification(private val context: Context) :
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class ObservingNotification(context: Context) :
|
class ObservingNotification(context: Context) :
|
||||||
ForegroundServiceNotification(context, INDEXER_CHANNEL) {
|
ForegroundServiceNotification(context, indexerChannel) {
|
||||||
init {
|
init {
|
||||||
setSmallIcon(R.drawable.ic_indexer_24)
|
setSmallIcon(R.drawable.ic_indexer_24)
|
||||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
|
|
@ -115,6 +115,6 @@ class ObservingNotification(context: Context) :
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Notification channel shared by [IndexingNotification] and [ObservingNotification]. */
|
/** Notification channel shared by [IndexingNotification] and [ObservingNotification]. */
|
||||||
private val INDEXER_CHANNEL =
|
private val indexerChannel =
|
||||||
ForegroundServiceNotification.ChannelInfo(
|
ForegroundServiceNotification.ChannelInfo(
|
||||||
id = BuildConfig.APPLICATION_ID + ".channel.INDEXER", nameRes = R.string.lbl_indexer)
|
id = BuildConfig.APPLICATION_ID + ".channel.INDEXER", nameRes = R.string.lbl_indexer)
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@
|
||||||
package org.oxycblt.auxio.music.user
|
package org.oxycblt.auxio.music.user
|
||||||
|
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
|
import org.oxycblt.auxio.music.MusicType
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.device.DeviceLibrary
|
import org.oxycblt.auxio.music.device.DeviceLibrary
|
||||||
|
|
@ -42,7 +42,9 @@ private constructor(
|
||||||
|
|
||||||
override fun equals(other: Any?) =
|
override fun equals(other: Any?) =
|
||||||
other is PlaylistImpl && uid == other.uid && name == other.name && songs == other.songs
|
other is PlaylistImpl && uid == other.uid && name == other.name && songs == other.songs
|
||||||
|
|
||||||
override fun hashCode() = hashCode
|
override fun hashCode() = hashCode
|
||||||
|
|
||||||
override fun toString() = "Playlist(uid=$uid, name=$name)"
|
override fun toString() = "Playlist(uid=$uid, name=$name)"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -78,7 +80,7 @@ private constructor(
|
||||||
*/
|
*/
|
||||||
fun from(name: String, songs: List<Song>, musicSettings: MusicSettings) =
|
fun from(name: String, songs: List<Song>, musicSettings: MusicSettings) =
|
||||||
PlaylistImpl(
|
PlaylistImpl(
|
||||||
Music.UID.auxio(MusicMode.PLAYLISTS),
|
Music.UID.auxio(MusicType.PLAYLISTS),
|
||||||
Name.Known.from(name, null, musicSettings),
|
Name.Known.from(name, null, musicSettings),
|
||||||
songs)
|
songs)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,9 @@ private class UserLibraryImpl(
|
||||||
private val musicSettings: MusicSettings
|
private val musicSettings: MusicSettings
|
||||||
) : MutableUserLibrary {
|
) : MutableUserLibrary {
|
||||||
override fun hashCode() = playlistMap.hashCode()
|
override fun hashCode() = playlistMap.hashCode()
|
||||||
|
|
||||||
override fun equals(other: Any?) = other is UserLibraryImpl && other.playlistMap == playlistMap
|
override fun equals(other: Any?) = other is UserLibraryImpl && other.playlistMap == playlistMap
|
||||||
|
|
||||||
override fun toString() = "UserLibrary(playlists=${playlists.size})"
|
override fun toString() = "UserLibrary(playlists=${playlists.size})"
|
||||||
|
|
||||||
override val playlists: Collection<Playlist>
|
override val playlists: Collection<Playlist>
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ abstract class UserMusicDatabase : RoomDatabase() {
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
@Dao
|
@Dao
|
||||||
interface PlaylistDao {
|
abstract class PlaylistDao {
|
||||||
/**
|
/**
|
||||||
* Read out all playlists stored in the database.
|
* Read out all playlists stored in the database.
|
||||||
*
|
*
|
||||||
|
|
@ -59,7 +59,7 @@ interface PlaylistDao {
|
||||||
*/
|
*/
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM PlaylistInfo")
|
@Query("SELECT * FROM PlaylistInfo")
|
||||||
suspend fun readRawPlaylists(): List<RawPlaylist>
|
abstract suspend fun readRawPlaylists(): List<RawPlaylist>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new playlist.
|
* Create a new playlist.
|
||||||
|
|
@ -67,7 +67,7 @@ interface PlaylistDao {
|
||||||
* @param rawPlaylist The [RawPlaylist] to create.
|
* @param rawPlaylist The [RawPlaylist] to create.
|
||||||
*/
|
*/
|
||||||
@Transaction
|
@Transaction
|
||||||
suspend fun insertPlaylist(rawPlaylist: RawPlaylist) {
|
open suspend fun insertPlaylist(rawPlaylist: RawPlaylist) {
|
||||||
insertInfo(rawPlaylist.playlistInfo)
|
insertInfo(rawPlaylist.playlistInfo)
|
||||||
insertSongs(rawPlaylist.songs)
|
insertSongs(rawPlaylist.songs)
|
||||||
insertRefs(
|
insertRefs(
|
||||||
|
|
@ -83,7 +83,7 @@ interface PlaylistDao {
|
||||||
* @param playlistInfo The new [PlaylistInfo] to store.
|
* @param playlistInfo The new [PlaylistInfo] to store.
|
||||||
*/
|
*/
|
||||||
@Transaction
|
@Transaction
|
||||||
suspend fun replacePlaylistInfo(playlistInfo: PlaylistInfo) {
|
open suspend fun replacePlaylistInfo(playlistInfo: PlaylistInfo) {
|
||||||
deleteInfo(playlistInfo.playlistUid)
|
deleteInfo(playlistInfo.playlistUid)
|
||||||
insertInfo(playlistInfo)
|
insertInfo(playlistInfo)
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +94,7 @@ interface PlaylistDao {
|
||||||
* @param playlistUid The [Music.UID] of the playlist to delete.
|
* @param playlistUid The [Music.UID] of the playlist to delete.
|
||||||
*/
|
*/
|
||||||
@Transaction
|
@Transaction
|
||||||
suspend fun deletePlaylist(playlistUid: Music.UID) {
|
open suspend fun deletePlaylist(playlistUid: Music.UID) {
|
||||||
deleteInfo(playlistUid)
|
deleteInfo(playlistUid)
|
||||||
deleteRefs(playlistUid)
|
deleteRefs(playlistUid)
|
||||||
}
|
}
|
||||||
|
|
@ -106,7 +106,7 @@ interface PlaylistDao {
|
||||||
* @param songs The [PlaylistSong] representing each song to put into the playlist.
|
* @param songs The [PlaylistSong] representing each song to put into the playlist.
|
||||||
*/
|
*/
|
||||||
@Transaction
|
@Transaction
|
||||||
suspend fun insertPlaylistSongs(playlistUid: Music.UID, songs: List<PlaylistSong>) {
|
open suspend fun insertPlaylistSongs(playlistUid: Music.UID, songs: List<PlaylistSong>) {
|
||||||
insertSongs(songs)
|
insertSongs(songs)
|
||||||
insertRefs(
|
insertRefs(
|
||||||
songs.map { PlaylistSongCrossRef(playlistUid = playlistUid, songUid = it.songUid) })
|
songs.map { PlaylistSongCrossRef(playlistUid = playlistUid, songUid = it.songUid) })
|
||||||
|
|
@ -120,7 +120,7 @@ interface PlaylistDao {
|
||||||
* playlist.
|
* playlist.
|
||||||
*/
|
*/
|
||||||
@Transaction
|
@Transaction
|
||||||
suspend fun replacePlaylistSongs(playlistUid: Music.UID, songs: List<PlaylistSong>) {
|
open suspend fun replacePlaylistSongs(playlistUid: Music.UID, songs: List<PlaylistSong>) {
|
||||||
deleteRefs(playlistUid)
|
deleteRefs(playlistUid)
|
||||||
insertSongs(songs)
|
insertSongs(songs)
|
||||||
insertRefs(
|
insertRefs(
|
||||||
|
|
@ -128,21 +128,22 @@ interface PlaylistDao {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Internal, do not use. */
|
/** Internal, do not use. */
|
||||||
@Insert(onConflict = OnConflictStrategy.ABORT) suspend fun insertInfo(info: PlaylistInfo)
|
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||||
|
abstract suspend fun insertInfo(info: PlaylistInfo)
|
||||||
|
|
||||||
/** Internal, do not use. */
|
/** Internal, do not use. */
|
||||||
@Query("DELETE FROM PlaylistInfo where playlistUid = :playlistUid")
|
@Query("DELETE FROM PlaylistInfo where playlistUid = :playlistUid")
|
||||||
suspend fun deleteInfo(playlistUid: Music.UID)
|
abstract suspend fun deleteInfo(playlistUid: Music.UID)
|
||||||
|
|
||||||
/** Internal, do not use. */
|
/** Internal, do not use. */
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
suspend fun insertSongs(songs: List<PlaylistSong>)
|
abstract suspend fun insertSongs(songs: List<PlaylistSong>)
|
||||||
|
|
||||||
/** Internal, do not use. */
|
/** Internal, do not use. */
|
||||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||||
suspend fun insertRefs(refs: List<PlaylistSongCrossRef>)
|
abstract suspend fun insertRefs(refs: List<PlaylistSongCrossRef>)
|
||||||
|
|
||||||
/** Internal, do not use. */
|
/** Internal, do not use. */
|
||||||
@Query("DELETE FROM PlaylistSongCrossRef where playlistUid = :playlistUid")
|
@Query("DELETE FROM PlaylistSongCrossRef where playlistUid = :playlistUid")
|
||||||
suspend fun deleteRefs(playlistUid: Music.UID)
|
abstract suspend fun deleteRefs(playlistUid: Music.UID)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
127
app/src/main/java/org/oxycblt/auxio/playback/PlaySong.kt
Normal file
127
app/src/main/java/org/oxycblt/auxio/playback/PlaySong.kt
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* PlaySong.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.playback
|
||||||
|
|
||||||
|
import org.oxycblt.auxio.IntegerTable
|
||||||
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
import org.oxycblt.auxio.music.Playlist
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration to play a song in a desired way.
|
||||||
|
*
|
||||||
|
* Since songs are not [MusicParent]s, the way the queue is generated around them has a lot more
|
||||||
|
* flexibility. The particular strategy used can be configured the user, but it also needs to be
|
||||||
|
* transferred between views at points (such as menus). [PlaySong] provides both of these, being a
|
||||||
|
* enum-like datatype when configuration is needed, and an algebraic datatype when data transfer is
|
||||||
|
* needed.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
sealed interface PlaySong {
|
||||||
|
/**
|
||||||
|
* The integer representation of this instance.
|
||||||
|
*
|
||||||
|
* @see fromIntCode
|
||||||
|
*/
|
||||||
|
val intCode: Int
|
||||||
|
|
||||||
|
/** Play a Song from the entire library of songs. */
|
||||||
|
data object FromAll : PlaySong {
|
||||||
|
override val intCode = IntegerTable.PLAY_SONG_FROM_ALL
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Play a song from it's album. */
|
||||||
|
data object FromAlbum : PlaySong {
|
||||||
|
override val intCode = IntegerTable.PLAY_SONG_FROM_ALBUM
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a song from (possibly) one of it's [Artist]s.
|
||||||
|
*
|
||||||
|
* @param which The [Artist] to specifically play from. If null, the user will be prompted for
|
||||||
|
* an [Artist] to choose of the song has multiple. Otherwise, the only [Artist] will be used.
|
||||||
|
*/
|
||||||
|
data class FromArtist(val which: Artist?) : PlaySong {
|
||||||
|
override val intCode = IntegerTable.PLAY_SONG_FROM_ARTIST
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a song from (possibly) one of it's [Genre]s.
|
||||||
|
*
|
||||||
|
* @param which The [Genre] to specifically play from. If null, the user will be prompted for a
|
||||||
|
* [Genre] to choose of the song has multiple. Otherwise, the only [Genre] will be used.
|
||||||
|
*/
|
||||||
|
data class FromGenre(val which: Genre?) : PlaySong {
|
||||||
|
override val intCode = IntegerTable.PLAY_SONG_FROM_GENRE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a song from one of it's [Playlist]s.
|
||||||
|
*
|
||||||
|
* @param which The [Playlist] to specifically play from. This must be provided.
|
||||||
|
*/
|
||||||
|
data class FromPlaylist(val which: Playlist) : PlaySong {
|
||||||
|
override val intCode = IntegerTable.PLAY_SONG_FROM_PLAYLIST
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Only play the given song, include nothing else in the queue. */
|
||||||
|
data object ByItself : PlaySong {
|
||||||
|
override val intCode = IntegerTable.PLAY_SONG_BY_ITSELF
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Convert a [PlaySong] integer representation into an instance.
|
||||||
|
*
|
||||||
|
* @param intCode An integer representation of a [PlaySong]
|
||||||
|
* @param which Optional [MusicParent] to automatically populate a [FromArtist],
|
||||||
|
* [FromGenre], or [FromPlaylist] instance. If the type of the [MusicParent] does not
|
||||||
|
* match, it will be considered invalid and null will be returned.
|
||||||
|
* @return The corresponding [PlaySong], or null if the [PlaySong] is invalid.
|
||||||
|
* @see PlaySong.intCode
|
||||||
|
*/
|
||||||
|
fun fromIntCode(intCode: Int, which: MusicParent? = null): PlaySong? =
|
||||||
|
when (intCode) {
|
||||||
|
IntegerTable.PLAY_SONG_BY_ITSELF -> ByItself
|
||||||
|
IntegerTable.PLAY_SONG_FROM_ALL -> FromAll
|
||||||
|
IntegerTable.PLAY_SONG_FROM_ALBUM -> FromAlbum
|
||||||
|
IntegerTable.PLAY_SONG_FROM_ARTIST ->
|
||||||
|
if (which is Artist?) {
|
||||||
|
FromArtist(which)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
IntegerTable.PLAY_SONG_FROM_GENRE ->
|
||||||
|
if (which is Genre?) {
|
||||||
|
FromGenre(which)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
IntegerTable.PLAY_SONG_FROM_PLAYLIST ->
|
||||||
|
if (which is Playlist) {
|
||||||
|
FromPlaylist(which)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,5 +32,6 @@ interface PlaybackModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
@Binds
|
@Binds
|
||||||
fun stateManager(playbackManager: PlaybackStateManagerImpl): PlaybackStateManager
|
fun stateManager(playbackManager: PlaybackStateManagerImpl): PlaybackStateManager
|
||||||
|
|
||||||
@Binds fun settings(playbackSettings: PlaybackSettingsImpl): PlaybackSettings
|
@Binds fun settings(playbackSettings: PlaybackSettingsImpl): PlaybackSettings
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
|
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
|
||||||
import org.oxycblt.auxio.detail.DetailViewModel
|
import org.oxycblt.auxio.detail.DetailViewModel
|
||||||
import org.oxycblt.auxio.detail.Show
|
import org.oxycblt.auxio.detail.Show
|
||||||
|
import org.oxycblt.auxio.list.ListViewModel
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.pager.PlaybackPageListener
|
import org.oxycblt.auxio.playback.pager.PlaybackPageListener
|
||||||
import org.oxycblt.auxio.playback.pager.PlaybackPagerAdapter
|
import org.oxycblt.auxio.playback.pager.PlaybackPagerAdapter
|
||||||
|
|
@ -52,7 +52,7 @@ import org.oxycblt.auxio.util.collect
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.lazyReflectedField
|
import org.oxycblt.auxio.util.lazyReflectedField
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.share
|
import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
|
||||||
|
|
@ -71,9 +71,9 @@ class PlaybackPanelFragment :
|
||||||
StyledSeekBar.Listener,
|
StyledSeekBar.Listener,
|
||||||
PlaybackPageListener {
|
PlaybackPageListener {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val musicModel: MusicViewModel by activityViewModels()
|
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
private val queueModel: QueueViewModel by activityViewModels()
|
private val queueModel: QueueViewModel by activityViewModels()
|
||||||
|
private val listModel: ListViewModel by activityViewModels()
|
||||||
private var equalizerLauncher: ActivityResultLauncher<Intent>? = null
|
private var equalizerLauncher: ActivityResultLauncher<Intent>? = null
|
||||||
private var coverAdapter: PlaybackPagerAdapter? = null
|
private var coverAdapter: PlaybackPagerAdapter? = null
|
||||||
|
|
||||||
|
|
@ -103,6 +103,13 @@ class PlaybackPanelFragment :
|
||||||
binding.playbackToolbar.apply {
|
binding.playbackToolbar.apply {
|
||||||
setNavigationOnClickListener { playbackModel.openMain() }
|
setNavigationOnClickListener { playbackModel.openMain() }
|
||||||
setOnMenuItemClickListener(this@PlaybackPanelFragment)
|
setOnMenuItemClickListener(this@PlaybackPanelFragment)
|
||||||
|
overrideOnOverflowMenuClick {
|
||||||
|
playbackModel.song.value?.let {
|
||||||
|
// No playback options are actually available in the menu, so use a junk
|
||||||
|
// PlaySong option.
|
||||||
|
listModel.openMenu(R.menu.item_playback_song, it, PlaySong.ByItself)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cover carousel adapter
|
// cover carousel adapter
|
||||||
|
|
@ -142,51 +149,28 @@ class PlaybackPanelFragment :
|
||||||
binding.playbackToolbar.setOnMenuItemClickListener(null)
|
binding.playbackToolbar.setOnMenuItemClickListener(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem) =
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
if (item.itemId == R.id.action_open_equalizer) {
|
||||||
R.id.action_open_equalizer -> {
|
|
||||||
// Launch the system equalizer app, if possible.
|
// Launch the system equalizer app, if possible.
|
||||||
logD("Launching equalizer")
|
logD("Launching equalizer")
|
||||||
val equalizerIntent =
|
val equalizerIntent =
|
||||||
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL)
|
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL)
|
||||||
// Provide audio session ID so the equalizer can show options for this app
|
// Provide audio session ID so the equalizer can show options for this app
|
||||||
// in particular.
|
// in particular.
|
||||||
.putExtra(
|
.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, playbackModel.currentAudioSessionId)
|
||||||
AudioEffect.EXTRA_AUDIO_SESSION, playbackModel.currentAudioSessionId)
|
|
||||||
// Signal music type so that the equalizer settings are appropriate for
|
// Signal music type so that the equalizer settings are appropriate for
|
||||||
// music playback.
|
// music playback.
|
||||||
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
|
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
|
||||||
try {
|
try {
|
||||||
requireNotNull(equalizerLauncher) {
|
requireNotNull(equalizerLauncher) { "Equalizer panel launcher was not available" }
|
||||||
"Equalizer panel launcher was not available"
|
|
||||||
}
|
|
||||||
.launch(equalizerIntent)
|
.launch(equalizerIntent)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
requireContext().showToast(R.string.err_no_app)
|
requireContext().showToast(R.string.err_no_app)
|
||||||
}
|
}
|
||||||
true
|
return true
|
||||||
}
|
}
|
||||||
R.id.action_artist_details -> {
|
|
||||||
navigateToCurrentArtist()
|
return false
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_album_details -> {
|
|
||||||
navigateToCurrentAlbum()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_playlist_add -> {
|
|
||||||
playbackModel.song.value?.let(musicModel::addToPlaylist)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_detail -> {
|
|
||||||
playbackModel.song.value?.let(detailModel::showSong)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_share -> {
|
|
||||||
playbackModel.song.value?.let { requireContext().share(it) }
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSeekConfirmed(positionDs: Long) {
|
override fun onSeekConfirmed(positionDs: Long) {
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
|
||||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
|
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
|
||||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp
|
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
|
|
@ -46,16 +45,13 @@ interface PlaybackSettings : Settings<PlaybackSettings.Listener> {
|
||||||
val replayGainMode: ReplayGainMode
|
val replayGainMode: ReplayGainMode
|
||||||
/** The current ReplayGain pre-amp configuration. */
|
/** The current ReplayGain pre-amp configuration. */
|
||||||
var replayGainPreAmp: ReplayGainPreAmp
|
var replayGainPreAmp: ReplayGainPreAmp
|
||||||
|
/** How to play a song from a general list of songs, specified by [PlaySong] */
|
||||||
|
val playInListWith: PlaySong
|
||||||
/**
|
/**
|
||||||
* What type of MusicParent to play from when a Song is played from a list of other items. Null
|
* How to play a song from a parent item, specified by [PlaySong]. Null if to delegate to the UI
|
||||||
* if to play from all Songs.
|
* context.
|
||||||
*/
|
*/
|
||||||
val inListPlaybackMode: MusicMode
|
val inParentPlaybackMode: PlaySong?
|
||||||
/**
|
|
||||||
* What type of MusicParent to play from when a Song is played from within an item (ex. like in
|
|
||||||
* the detail view). Null if to play from the item it was played in.
|
|
||||||
*/
|
|
||||||
val inParentPlaybackMode: MusicMode?
|
|
||||||
/** Whether to keep shuffle on when playing a new Song. */
|
/** Whether to keep shuffle on when playing a new Song. */
|
||||||
val keepShuffle: Boolean
|
val keepShuffle: Boolean
|
||||||
/** Whether to rewind when the skip previous button is pressed before skipping back. */
|
/** Whether to rewind when the skip previous button is pressed before skipping back. */
|
||||||
|
|
@ -75,18 +71,18 @@ interface PlaybackSettings : Settings<PlaybackSettings.Listener> {
|
||||||
|
|
||||||
class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Context) :
|
class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Context) :
|
||||||
Settings.Impl<PlaybackSettings.Listener>(context), PlaybackSettings {
|
Settings.Impl<PlaybackSettings.Listener>(context), PlaybackSettings {
|
||||||
override val inListPlaybackMode: MusicMode
|
override val playInListWith: PlaySong
|
||||||
get() =
|
get() =
|
||||||
MusicMode.fromIntCode(
|
PlaySong.fromIntCode(
|
||||||
sharedPreferences.getInt(
|
sharedPreferences.getInt(
|
||||||
getString(R.string.set_key_in_list_playback_mode), Int.MIN_VALUE))
|
getString(R.string.set_key_play_in_list_with), Int.MIN_VALUE))
|
||||||
?: MusicMode.SONGS
|
?: PlaySong.FromAll
|
||||||
|
|
||||||
override val inParentPlaybackMode: MusicMode?
|
override val inParentPlaybackMode: PlaySong?
|
||||||
get() =
|
get() =
|
||||||
MusicMode.fromIntCode(
|
PlaySong.fromIntCode(
|
||||||
sharedPreferences.getInt(
|
sharedPreferences.getInt(
|
||||||
getString(R.string.set_key_in_parent_playback_mode), Int.MIN_VALUE))
|
getString(R.string.set_key_play_in_parent_with), Int.MIN_VALUE))
|
||||||
|
|
||||||
override val barAction: ActionMode
|
override val barAction: ActionMode
|
||||||
get() =
|
get() =
|
||||||
|
|
@ -132,65 +128,44 @@ class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Cont
|
||||||
get() = sharedPreferences.getBoolean(getString(R.string.set_key_repeat_pause), false)
|
get() = sharedPreferences.getBoolean(getString(R.string.set_key_repeat_pause), false)
|
||||||
|
|
||||||
override fun migrate() {
|
override fun migrate() {
|
||||||
// "Use alternate notification action" was converted to an ActionMode setting in 3.0.0.
|
// MusicMode was converted to PlaySong in 3.2.0
|
||||||
if (sharedPreferences.contains(OLD_KEY_ALT_NOTIF_ACTION)) {
|
fun Int.migrateMusicMode() =
|
||||||
logD("Migrating $OLD_KEY_ALT_NOTIF_ACTION")
|
|
||||||
|
|
||||||
val mode =
|
|
||||||
if (sharedPreferences.getBoolean(OLD_KEY_ALT_NOTIF_ACTION, false)) {
|
|
||||||
ActionMode.SHUFFLE
|
|
||||||
} else {
|
|
||||||
ActionMode.REPEAT
|
|
||||||
}
|
|
||||||
|
|
||||||
sharedPreferences.edit {
|
|
||||||
putInt(getString(R.string.set_key_notif_action), mode.intCode)
|
|
||||||
remove(OLD_KEY_ALT_NOTIF_ACTION)
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlaybackMode was converted to MusicMode in 3.0.0
|
|
||||||
|
|
||||||
fun Int.migratePlaybackMode() =
|
|
||||||
when (this) {
|
when (this) {
|
||||||
// Convert PlaybackMode into MusicMode
|
IntegerTable.MUSIC_MODE_SONGS -> PlaySong.FromAll
|
||||||
IntegerTable.PLAYBACK_MODE_ALL_SONGS -> MusicMode.SONGS
|
IntegerTable.MUSIC_MODE_ALBUMS -> PlaySong.FromAlbum
|
||||||
IntegerTable.PLAYBACK_MODE_IN_ARTIST -> MusicMode.ARTISTS
|
IntegerTable.MUSIC_MODE_ARTISTS -> PlaySong.FromArtist(null)
|
||||||
IntegerTable.PLAYBACK_MODE_IN_ALBUM -> MusicMode.ALBUMS
|
IntegerTable.MUSIC_MODE_GENRES -> PlaySong.FromGenre(null)
|
||||||
IntegerTable.PLAYBACK_MODE_IN_GENRE -> MusicMode.GENRES
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sharedPreferences.contains(OLD_KEY_LIB_PLAYBACK_MODE)) {
|
if (sharedPreferences.contains(OLD_KEY_LIB_MUSIC_PLAYBACK_MODE)) {
|
||||||
logD("Migrating $OLD_KEY_LIB_PLAYBACK_MODE")
|
logD("Migrating $OLD_KEY_LIB_MUSIC_PLAYBACK_MODE")
|
||||||
|
|
||||||
val mode =
|
val mode =
|
||||||
sharedPreferences
|
sharedPreferences
|
||||||
.getInt(OLD_KEY_LIB_PLAYBACK_MODE, IntegerTable.PLAYBACK_MODE_ALL_SONGS)
|
.getInt(OLD_KEY_LIB_MUSIC_PLAYBACK_MODE, Int.MIN_VALUE)
|
||||||
.migratePlaybackMode()
|
.migrateMusicMode()
|
||||||
?: MusicMode.SONGS
|
|
||||||
|
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
putInt(getString(R.string.set_key_in_list_playback_mode), mode.intCode)
|
putInt(
|
||||||
remove(OLD_KEY_LIB_PLAYBACK_MODE)
|
getString(R.string.set_key_play_in_list_with), mode?.intCode ?: Int.MIN_VALUE)
|
||||||
|
remove(OLD_KEY_LIB_MUSIC_PLAYBACK_MODE)
|
||||||
apply()
|
apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sharedPreferences.contains(OLD_KEY_DETAIL_PLAYBACK_MODE)) {
|
if (sharedPreferences.contains(OLD_KEY_DETAIL_MUSIC_PLAYBACK_MODE)) {
|
||||||
logD("Migrating $OLD_KEY_DETAIL_PLAYBACK_MODE")
|
logD("Migrating $OLD_KEY_DETAIL_MUSIC_PLAYBACK_MODE")
|
||||||
|
|
||||||
val mode =
|
val mode =
|
||||||
sharedPreferences
|
sharedPreferences
|
||||||
.getInt(OLD_KEY_DETAIL_PLAYBACK_MODE, Int.MIN_VALUE)
|
.getInt(OLD_KEY_DETAIL_MUSIC_PLAYBACK_MODE, Int.MIN_VALUE)
|
||||||
.migratePlaybackMode()
|
.migrateMusicMode()
|
||||||
|
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
putInt(
|
putInt(
|
||||||
getString(R.string.set_key_in_parent_playback_mode),
|
getString(R.string.set_key_play_in_parent_with), mode?.intCode ?: Int.MIN_VALUE)
|
||||||
mode?.intCode ?: Int.MIN_VALUE)
|
remove(OLD_KEY_DETAIL_MUSIC_PLAYBACK_MODE)
|
||||||
remove(OLD_KEY_DETAIL_PLAYBACK_MODE)
|
|
||||||
apply()
|
apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -216,8 +191,7 @@ class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Cont
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val OLD_KEY_ALT_NOTIF_ACTION = "KEY_ALT_NOTIF_ACTION"
|
const val OLD_KEY_LIB_MUSIC_PLAYBACK_MODE = "auxio_library_playback_mode"
|
||||||
const val OLD_KEY_LIB_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2"
|
const val OLD_KEY_DETAIL_MUSIC_PLAYBACK_MODE = "auxio_detail_playback_mode"
|
||||||
const val OLD_KEY_DETAIL_PLAYBACK_MODE = "auxio_detail_song_play_mode"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,12 @@ import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.oxycblt.auxio.list.ListSettings
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.persist.PersistenceRepository
|
import org.oxycblt.auxio.playback.persist.PersistenceRepository
|
||||||
|
|
@ -59,8 +58,8 @@ constructor(
|
||||||
private val playbackManager: PlaybackStateManager,
|
private val playbackManager: PlaybackStateManager,
|
||||||
private val playbackSettings: PlaybackSettings,
|
private val playbackSettings: PlaybackSettings,
|
||||||
private val persistenceRepository: PersistenceRepository,
|
private val persistenceRepository: PersistenceRepository,
|
||||||
|
private val listSettings: ListSettings,
|
||||||
private val musicRepository: MusicRepository,
|
private val musicRepository: MusicRepository,
|
||||||
private val musicSettings: MusicSettings
|
|
||||||
) : ViewModel(), PlaybackStateManager.Listener, PlaybackSettings.Listener {
|
) : ViewModel(), PlaybackStateManager.Listener, PlaybackSettings.Listener {
|
||||||
private var lastPositionJob: Job? = null
|
private var lastPositionJob: Job? = null
|
||||||
|
|
||||||
|
|
@ -68,6 +67,7 @@ constructor(
|
||||||
/** The currently playing song. */
|
/** The currently playing song. */
|
||||||
val song: StateFlow<Song?>
|
val song: StateFlow<Song?>
|
||||||
get() = _song
|
get() = _song
|
||||||
|
|
||||||
private val _parent = MutableStateFlow<MusicParent?>(null)
|
private val _parent = MutableStateFlow<MusicParent?>(null)
|
||||||
/** The [MusicParent] currently being played. Null if playback is occurring from all songs. */
|
/** The [MusicParent] currently being played. Null if playback is occurring from all songs. */
|
||||||
val parent: StateFlow<MusicParent?> = _parent
|
val parent: StateFlow<MusicParent?> = _parent
|
||||||
|
|
@ -75,6 +75,7 @@ constructor(
|
||||||
/** Whether playback is ongoing or paused. */
|
/** Whether playback is ongoing or paused. */
|
||||||
val isPlaying: StateFlow<Boolean>
|
val isPlaying: StateFlow<Boolean>
|
||||||
get() = _isPlaying
|
get() = _isPlaying
|
||||||
|
|
||||||
private val _positionDs = MutableStateFlow(0L)
|
private val _positionDs = MutableStateFlow(0L)
|
||||||
/** The current position, in deci-seconds (1/10th of a second). */
|
/** The current position, in deci-seconds (1/10th of a second). */
|
||||||
val positionDs: StateFlow<Long>
|
val positionDs: StateFlow<Long>
|
||||||
|
|
@ -84,6 +85,7 @@ constructor(
|
||||||
/** The current [RepeatMode]. */
|
/** The current [RepeatMode]. */
|
||||||
val repeatMode: StateFlow<RepeatMode>
|
val repeatMode: StateFlow<RepeatMode>
|
||||||
get() = _repeatMode
|
get() = _repeatMode
|
||||||
|
|
||||||
private val _isShuffled = MutableStateFlow(false)
|
private val _isShuffled = MutableStateFlow(false)
|
||||||
/** Whether the queue is shuffled or not. */
|
/** Whether the queue is shuffled or not. */
|
||||||
val isShuffled: StateFlow<Boolean>
|
val isShuffled: StateFlow<Boolean>
|
||||||
|
|
@ -180,32 +182,23 @@ constructor(
|
||||||
|
|
||||||
// --- PLAYING FUNCTIONS ---
|
// --- PLAYING FUNCTIONS ---
|
||||||
|
|
||||||
|
fun play(song: Song, with: PlaySong) {
|
||||||
|
logD("Playing $song with $with")
|
||||||
|
playWithImpl(song, with, isImplicitlyShuffled())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun playExplicit(song: Song, with: PlaySong) {
|
||||||
|
playWithImpl(song, with, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shuffleExplicit(song: Song, with: PlaySong) {
|
||||||
|
playWithImpl(song, with, true)
|
||||||
|
}
|
||||||
|
|
||||||
/** Shuffle all songs in the music library. */
|
/** Shuffle all songs in the music library. */
|
||||||
fun shuffleAll() {
|
fun shuffleAll() {
|
||||||
logD("Shuffling all songs")
|
logD("Shuffling all songs")
|
||||||
playImpl(null, null, true)
|
playFromAllImpl(null, true)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Play a [Song] from the [MusicParent] outlined by the given [MusicMode].
|
|
||||||
* - If [MusicMode.SONGS], the [Song] is played from all songs.
|
|
||||||
* - If [MusicMode.ALBUMS], the [Song] is played from it's [Album].
|
|
||||||
* - If [MusicMode.ARTISTS], the [Song] is played from one of it's [Artist]s.
|
|
||||||
* - If [MusicMode.GENRES], the [Song] is played from one of it's [Genre]s.
|
|
||||||
* [MusicMode.PLAYLISTS] is disallowed here.
|
|
||||||
*
|
|
||||||
* @param song The [Song] to play.
|
|
||||||
* @param playbackMode The [MusicMode] to play from.
|
|
||||||
*/
|
|
||||||
fun playFrom(song: Song, playbackMode: MusicMode) {
|
|
||||||
logD("Playing $song from $playbackMode")
|
|
||||||
when (playbackMode) {
|
|
||||||
MusicMode.SONGS -> playImpl(song, null)
|
|
||||||
MusicMode.ALBUMS -> playImpl(song, song.album)
|
|
||||||
MusicMode.ARTISTS -> playFromArtist(song)
|
|
||||||
MusicMode.GENRES -> playFromGenre(song)
|
|
||||||
MusicMode.PLAYLISTS -> error("Playing from a playlist is not supported.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -216,16 +209,7 @@ constructor(
|
||||||
* be prompted on what artist to play. Defaults to null.
|
* be prompted on what artist to play. Defaults to null.
|
||||||
*/
|
*/
|
||||||
fun playFromArtist(song: Song, artist: Artist? = null) {
|
fun playFromArtist(song: Song, artist: Artist? = null) {
|
||||||
if (artist != null) {
|
playFromArtistImpl(song, artist, isImplicitlyShuffled())
|
||||||
logD("Playing $song from $artist")
|
|
||||||
playImpl(song, artist)
|
|
||||||
} else if (song.artists.size == 1) {
|
|
||||||
logD("$song has one artist, playing from it")
|
|
||||||
playImpl(song, song.artists[0])
|
|
||||||
} else {
|
|
||||||
logD("$song has multiple artists, showing choice dialog")
|
|
||||||
startPlaybackDecisionImpl(PlaybackDecision.PlayFromArtist(song))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -236,19 +220,67 @@ constructor(
|
||||||
* be prompted on what artist to play. Defaults to null.
|
* be prompted on what artist to play. Defaults to null.
|
||||||
*/
|
*/
|
||||||
fun playFromGenre(song: Song, genre: Genre? = null) {
|
fun playFromGenre(song: Song, genre: Genre? = null) {
|
||||||
if (genre != null) {
|
playFromGenreImpl(song, genre, isImplicitlyShuffled())
|
||||||
logD("Playing $song from $genre")
|
}
|
||||||
playImpl(song, genre)
|
|
||||||
} else if (song.genres.size == 1) {
|
private fun isImplicitlyShuffled() =
|
||||||
logD("$song has one genre, playing from it")
|
playbackManager.queue.isShuffled && playbackSettings.keepShuffle
|
||||||
playImpl(song, song.genres[0])
|
|
||||||
} else {
|
private fun playWithImpl(song: Song, with: PlaySong, shuffled: Boolean) {
|
||||||
logD("$song has multiple genres, showing choice dialog")
|
when (with) {
|
||||||
startPlaybackDecisionImpl(PlaybackDecision.PlayFromGenre(song))
|
is PlaySong.FromAll -> playFromAllImpl(song, shuffled)
|
||||||
|
is PlaySong.FromAlbum -> playFromAlbumImpl(song, shuffled)
|
||||||
|
is PlaySong.FromArtist -> playFromArtistImpl(song, with.which, shuffled)
|
||||||
|
is PlaySong.FromGenre -> playFromGenreImpl(song, with.which, shuffled)
|
||||||
|
is PlaySong.FromPlaylist -> playFromPlaylistImpl(song, with.which, shuffled)
|
||||||
|
is PlaySong.ByItself -> playItselfImpl(song, shuffled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startPlaybackDecisionImpl(decision: PlaybackDecision) {
|
private fun playFromAllImpl(song: Song?, shuffled: Boolean) {
|
||||||
|
playImpl(song, null, shuffled)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playFromAlbumImpl(song: Song, shuffled: Boolean) {
|
||||||
|
playImpl(song, song.album, shuffled)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playFromArtistImpl(song: Song, artist: Artist?, shuffled: Boolean) {
|
||||||
|
if (artist != null) {
|
||||||
|
logD("Playing $song from $artist")
|
||||||
|
playImpl(song, artist, shuffled)
|
||||||
|
} else if (song.artists.size == 1) {
|
||||||
|
logD("$song has one artist, playing from it")
|
||||||
|
playImpl(song, song.artists[0], shuffled)
|
||||||
|
} else {
|
||||||
|
logD("$song has multiple artists, showing choice dialog")
|
||||||
|
startPlaybackDecision(PlaybackDecision.PlayFromArtist(song))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playFromGenreImpl(song: Song, genre: Genre?, shuffled: Boolean) {
|
||||||
|
if (genre != null) {
|
||||||
|
logD("Playing $song from $genre")
|
||||||
|
playImpl(song, genre, shuffled)
|
||||||
|
} else if (song.genres.size == 1) {
|
||||||
|
logD("$song has one genre, playing from it")
|
||||||
|
playImpl(song, song.genres[0], shuffled)
|
||||||
|
} else {
|
||||||
|
logD("$song has multiple genres, showing choice dialog")
|
||||||
|
startPlaybackDecision(PlaybackDecision.PlayFromGenre(song))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playFromPlaylistImpl(song: Song, playlist: Playlist, shuffled: Boolean) {
|
||||||
|
logD("Playing $song from $playlist")
|
||||||
|
playImpl(song, playlist, shuffled)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playItselfImpl(song: Song, shuffled: Boolean) {
|
||||||
|
playImpl(song, listOf(song), shuffled)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startPlaybackDecision(decision: PlaybackDecision) {
|
||||||
val existing = _playbackDecision.flow.value
|
val existing = _playbackDecision.flow.value
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
logD("Already handling decision $existing, ignoring $decision")
|
logD("Already handling decision $existing, ignoring $decision")
|
||||||
|
|
@ -257,17 +289,6 @@ constructor(
|
||||||
_playbackDecision.put(decision)
|
_playbackDecision.put(decision)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* PLay a [Song] from one of it's [Playlist]s.
|
|
||||||
*
|
|
||||||
* @param song The [Song] to play.
|
|
||||||
* @param playlist The [Playlist] to play from. Must be linked to the [Song].
|
|
||||||
*/
|
|
||||||
fun playFromPlaylist(song: Song, playlist: Playlist) {
|
|
||||||
logD("Playing $song from $playlist")
|
|
||||||
playImpl(song, playlist)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play an [Album].
|
* Play an [Album].
|
||||||
*
|
*
|
||||||
|
|
@ -278,46 +299,6 @@ constructor(
|
||||||
playImpl(null, album, false)
|
playImpl(null, album, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Play an [Artist].
|
|
||||||
*
|
|
||||||
* @param artist The [Artist] to play.
|
|
||||||
*/
|
|
||||||
fun play(artist: Artist) {
|
|
||||||
logD("Playing $artist")
|
|
||||||
playImpl(null, artist, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Play a [Genre].
|
|
||||||
*
|
|
||||||
* @param genre The [Genre] to play.
|
|
||||||
*/
|
|
||||||
fun play(genre: Genre) {
|
|
||||||
logD("Playing $genre")
|
|
||||||
playImpl(null, genre, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Play a [Playlist].
|
|
||||||
*
|
|
||||||
* @param playlist The [Playlist] to play.
|
|
||||||
*/
|
|
||||||
fun play(playlist: Playlist) {
|
|
||||||
logD("Playing $playlist")
|
|
||||||
playImpl(null, playlist, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Play a list of [Song]s.
|
|
||||||
*
|
|
||||||
* @param songs The [Song]s to play.
|
|
||||||
*/
|
|
||||||
fun play(songs: List<Song>) {
|
|
||||||
logD("Playing ${songs.size} songs")
|
|
||||||
playbackManager.play(null, null, songs, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shuffle an [Album].
|
* Shuffle an [Album].
|
||||||
*
|
*
|
||||||
|
|
@ -328,6 +309,16 @@ constructor(
|
||||||
playImpl(null, album, true)
|
playImpl(null, album, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play an [Artist].
|
||||||
|
*
|
||||||
|
* @param artist The [Artist] to play.
|
||||||
|
*/
|
||||||
|
fun play(artist: Artist) {
|
||||||
|
logD("Playing $artist")
|
||||||
|
playImpl(null, artist, false)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shuffle an [Artist].
|
* Shuffle an [Artist].
|
||||||
*
|
*
|
||||||
|
|
@ -338,6 +329,16 @@ constructor(
|
||||||
playImpl(null, artist, true)
|
playImpl(null, artist, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a [Genre].
|
||||||
|
*
|
||||||
|
* @param genre The [Genre] to play.
|
||||||
|
*/
|
||||||
|
fun play(genre: Genre) {
|
||||||
|
logD("Playing $genre")
|
||||||
|
playImpl(null, genre, false)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shuffle a [Genre].
|
* Shuffle a [Genre].
|
||||||
*
|
*
|
||||||
|
|
@ -348,6 +349,16 @@ constructor(
|
||||||
playImpl(null, genre, true)
|
playImpl(null, genre, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a [Playlist].
|
||||||
|
*
|
||||||
|
* @param playlist The [Playlist] to play.
|
||||||
|
*/
|
||||||
|
fun play(playlist: Playlist) {
|
||||||
|
logD("Playing $playlist")
|
||||||
|
playImpl(null, playlist, false)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shuffle a [Playlist].
|
* Shuffle a [Playlist].
|
||||||
*
|
*
|
||||||
|
|
@ -358,6 +369,16 @@ constructor(
|
||||||
playImpl(null, playlist, true)
|
playImpl(null, playlist, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a list of [Song]s.
|
||||||
|
*
|
||||||
|
* @param songs The [Song]s to play.
|
||||||
|
*/
|
||||||
|
fun play(songs: List<Song>) {
|
||||||
|
logD("Playing ${songs.size} songs")
|
||||||
|
playbackManager.play(null, null, songs, false)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shuffle a list of [Song]s.
|
* Shuffle a list of [Song]s.
|
||||||
*
|
*
|
||||||
|
|
@ -368,22 +389,23 @@ constructor(
|
||||||
playbackManager.play(null, null, songs, true)
|
playbackManager.play(null, null, songs, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playImpl(
|
private fun playImpl(song: Song?, queue: List<Song>, shuffled: Boolean) {
|
||||||
song: Song?,
|
check(song == null || queue.contains(song)) { "Song to play not in queue" }
|
||||||
parent: MusicParent?,
|
playbackManager.play(song, null, queue, shuffled)
|
||||||
shuffled: Boolean = playbackManager.queue.isShuffled && playbackSettings.keepShuffle
|
}
|
||||||
) {
|
|
||||||
|
private fun playImpl(song: Song?, parent: MusicParent?, shuffled: Boolean) {
|
||||||
check(song == null || parent == null || parent.songs.contains(song)) {
|
check(song == null || parent == null || parent.songs.contains(song)) {
|
||||||
"Song to play not in parent"
|
"Song to play not in parent"
|
||||||
}
|
}
|
||||||
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
||||||
val queue =
|
val queue =
|
||||||
when (parent) {
|
when (parent) {
|
||||||
is Genre -> musicSettings.genreSongSort.songs(parent.songs)
|
is Genre -> listSettings.genreSongSort.songs(parent.songs)
|
||||||
is Artist -> musicSettings.artistSongSort.songs(parent.songs)
|
is Artist -> listSettings.artistSongSort.songs(parent.songs)
|
||||||
is Album -> musicSettings.albumSongSort.songs(parent.songs)
|
is Album -> listSettings.albumSongSort.songs(parent.songs)
|
||||||
is Playlist -> parent.songs
|
is Playlist -> parent.songs
|
||||||
null -> musicSettings.songSort.songs(deviceLibrary.songs)
|
null -> listSettings.songSort.songs(deviceLibrary.songs)
|
||||||
}
|
}
|
||||||
playbackManager.play(song, parent, queue, shuffled)
|
playbackManager.play(song, parent, queue, shuffled)
|
||||||
}
|
}
|
||||||
|
|
@ -442,7 +464,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun playNext(album: Album) {
|
fun playNext(album: Album) {
|
||||||
logD("Playing $album next")
|
logD("Playing $album next")
|
||||||
playbackManager.playNext(musicSettings.albumSongSort.songs(album.songs))
|
playbackManager.playNext(listSettings.albumSongSort.songs(album.songs))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -452,7 +474,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun playNext(artist: Artist) {
|
fun playNext(artist: Artist) {
|
||||||
logD("Playing $artist next")
|
logD("Playing $artist next")
|
||||||
playbackManager.playNext(musicSettings.artistSongSort.songs(artist.songs))
|
playbackManager.playNext(listSettings.artistSongSort.songs(artist.songs))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -462,7 +484,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun playNext(genre: Genre) {
|
fun playNext(genre: Genre) {
|
||||||
logD("Playing $genre next")
|
logD("Playing $genre next")
|
||||||
playbackManager.playNext(musicSettings.genreSongSort.songs(genre.songs))
|
playbackManager.playNext(listSettings.genreSongSort.songs(genre.songs))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -502,7 +524,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun addToQueue(album: Album) {
|
fun addToQueue(album: Album) {
|
||||||
logD("Adding $album to queue")
|
logD("Adding $album to queue")
|
||||||
playbackManager.addToQueue(musicSettings.albumSongSort.songs(album.songs))
|
playbackManager.addToQueue(listSettings.albumSongSort.songs(album.songs))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -512,7 +534,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun addToQueue(artist: Artist) {
|
fun addToQueue(artist: Artist) {
|
||||||
logD("Adding $artist to queue")
|
logD("Adding $artist to queue")
|
||||||
playbackManager.addToQueue(musicSettings.artistSongSort.songs(artist.songs))
|
playbackManager.addToQueue(listSettings.artistSongSort.songs(artist.songs))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -522,7 +544,7 @@ constructor(
|
||||||
*/
|
*/
|
||||||
fun addToQueue(genre: Genre) {
|
fun addToQueue(genre: Genre) {
|
||||||
logD("Adding $genre to queue")
|
logD("Adding $genre to queue")
|
||||||
playbackManager.addToQueue(musicSettings.genreSongSort.songs(genre.songs))
|
playbackManager.addToQueue(listSettings.genreSongSort.songs(genre.songs))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -114,12 +114,14 @@ class MutableQueue : Queue {
|
||||||
@Volatile
|
@Volatile
|
||||||
override var index = -1
|
override var index = -1
|
||||||
private set
|
private set
|
||||||
|
|
||||||
override val currentSong: Song?
|
override val currentSong: Song?
|
||||||
get() =
|
get() =
|
||||||
shuffledMapping
|
shuffledMapping
|
||||||
.ifEmpty { orderedMapping.ifEmpty { null } }
|
.ifEmpty { orderedMapping.ifEmpty { null } }
|
||||||
?.getOrNull(index)
|
?.getOrNull(index)
|
||||||
?.let(heap::get)
|
?.let(heap::get)
|
||||||
|
|
||||||
override val isShuffled: Boolean
|
override val isShuffled: Boolean
|
||||||
get() = shuffledMapping.isNotEmpty()
|
get() = shuffledMapping.isNotEmpty()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import androidx.media3.common.C
|
||||||
import androidx.media3.common.Format
|
import androidx.media3.common.Format
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.audio.AudioProcessor
|
import androidx.media3.common.audio.AudioProcessor
|
||||||
import androidx.media3.exoplayer.audio.BaseAudioProcessor
|
import androidx.media3.common.audio.BaseAudioProcessor
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
|
|
@ -81,6 +81,7 @@ constructor(
|
||||||
applyReplayGain(queue.currentSong)
|
applyReplayGain(queue.currentSong)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
|
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
|
||||||
logD("New playback started, updating playback information")
|
logD("New playback started, updating playback information")
|
||||||
applyReplayGain(queue.currentSong)
|
applyReplayGain(queue.currentSong)
|
||||||
|
|
|
||||||
|
|
@ -77,13 +77,13 @@ interface InternalPlayer {
|
||||||
/** Possible long-running background tasks handled by the background playback task. */
|
/** Possible long-running background tasks handled by the background playback task. */
|
||||||
sealed interface Action {
|
sealed interface Action {
|
||||||
/** Restore the previously saved playback state. */
|
/** Restore the previously saved playback state. */
|
||||||
object RestoreState : Action
|
data object RestoreState : Action
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start shuffled playback of the entire music library. Analogous to the "Shuffle All"
|
* Start shuffled playback of the entire music library. Analogous to the "Shuffle All"
|
||||||
* shortcut.
|
* shortcut.
|
||||||
*/
|
*/
|
||||||
object ShuffleAll : Action
|
data object ShuffleAll : Action
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start playing an audio file at the given [Uri].
|
* Start playing an audio file at the given [Uri].
|
||||||
|
|
|
||||||
|
|
@ -310,15 +310,18 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
|
||||||
@Volatile
|
@Volatile
|
||||||
override var parent: MusicParent? = null
|
override var parent: MusicParent? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
override var playerState = InternalPlayer.State.from(isPlaying = false, isAdvancing = false, 0)
|
override var playerState = InternalPlayer.State.from(isPlaying = false, isAdvancing = false, 0)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
override var repeatMode = RepeatMode.NONE
|
override var repeatMode = RepeatMode.NONE
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
notifyRepeatModeChanged()
|
notifyRepeatModeChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
override val currentAudioSessionId: Int?
|
override val currentAudioSessionId: Int?
|
||||||
get() = internalPlayer?.audioSessionId
|
get() = internalPlayer?.audioSessionId
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import org.oxycblt.auxio.util.logD
|
||||||
class MediaButtonReceiver : BroadcastReceiver() {
|
class MediaButtonReceiver : BroadcastReceiver() {
|
||||||
@Inject lateinit var playbackManager: PlaybackStateManager
|
@Inject lateinit var playbackManager: PlaybackStateManager
|
||||||
|
|
||||||
|
// TODO: Figure this out
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
if (playbackManager.queue.currentSong != null) {
|
if (playbackManager.queue.currentSong != null) {
|
||||||
// We have a song, so we can assume that the service will start a foreground state.
|
// We have a song, so we can assume that the service will start a foreground state.
|
||||||
|
|
|
||||||
|
|
@ -339,7 +339,7 @@ constructor(
|
||||||
object : BitmapProvider.Target {
|
object : BitmapProvider.Target {
|
||||||
override fun onCompleted(bitmap: Bitmap?) {
|
override fun onCompleted(bitmap: Bitmap?) {
|
||||||
this@MediaSessionComponent.logD(
|
this@MediaSessionComponent.logD(
|
||||||
"Bitmap loaded, applying media " + "session and posting notification")
|
"Bitmap loaded, applying media session and posting notification")
|
||||||
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap)
|
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap)
|
||||||
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap)
|
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap)
|
||||||
val metadata = builder.build()
|
val metadata = builder.build()
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.audiofx.AudioEffect
|
import android.media.audiofx.AudioEffect
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.media3.common.AudioAttributes
|
import androidx.media3.common.AudioAttributes
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
|
|
@ -47,8 +47,8 @@ import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
|
import org.oxycblt.auxio.list.ListSettings
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
import org.oxycblt.auxio.playback.persist.PersistenceRepository
|
import org.oxycblt.auxio.playback.persist.PersistenceRepository
|
||||||
|
|
@ -99,8 +99,8 @@ class PlaybackService :
|
||||||
@Inject lateinit var playbackManager: PlaybackStateManager
|
@Inject lateinit var playbackManager: PlaybackStateManager
|
||||||
@Inject lateinit var playbackSettings: PlaybackSettings
|
@Inject lateinit var playbackSettings: PlaybackSettings
|
||||||
@Inject lateinit var persistenceRepository: PersistenceRepository
|
@Inject lateinit var persistenceRepository: PersistenceRepository
|
||||||
|
@Inject lateinit var listSettings: ListSettings
|
||||||
@Inject lateinit var musicRepository: MusicRepository
|
@Inject lateinit var musicRepository: MusicRepository
|
||||||
@Inject lateinit var musicSettings: MusicSettings
|
|
||||||
|
|
||||||
// State
|
// State
|
||||||
private lateinit var foregroundManager: ForegroundManager
|
private lateinit var foregroundManager: ForegroundManager
|
||||||
|
|
@ -165,18 +165,8 @@ class PlaybackService :
|
||||||
addAction(WidgetProvider.ACTION_WIDGET_UPDATE)
|
addAction(WidgetProvider.ACTION_WIDGET_UPDATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
ContextCompat.registerReceiver(
|
||||||
registerReceiver(
|
this, systemReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||||
systemReceiver,
|
|
||||||
intentFilter,
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
RECEIVER_NOT_EXPORTED
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
registerReceiver(systemReceiver, intentFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
logD("Service created")
|
logD("Service created")
|
||||||
}
|
}
|
||||||
|
|
@ -379,7 +369,7 @@ class PlaybackService :
|
||||||
is InternalPlayer.Action.ShuffleAll -> {
|
is InternalPlayer.Action.ShuffleAll -> {
|
||||||
logD("Shuffling all tracks")
|
logD("Shuffling all tracks")
|
||||||
playbackManager.play(
|
playbackManager.play(
|
||||||
null, null, musicSettings.songSort.songs(deviceLibrary.songs), true)
|
null, null, listSettings.songSort.songs(deviceLibrary.songs), true)
|
||||||
}
|
}
|
||||||
// Open -> Try to find the Song for the given file and then play it from all songs
|
// Open -> Try to find the Song for the given file and then play it from all songs
|
||||||
is InternalPlayer.Action.Open -> {
|
is InternalPlayer.Action.Open -> {
|
||||||
|
|
@ -388,7 +378,7 @@ class PlaybackService :
|
||||||
playbackManager.play(
|
playbackManager.play(
|
||||||
song,
|
song,
|
||||||
null,
|
null,
|
||||||
musicSettings.songSort.songs(deviceLibrary.songs),
|
listSettings.songSort.songs(deviceLibrary.songs),
|
||||||
playbackManager.queue.isShuffled && playbackSettings.keepShuffle)
|
playbackManager.queue.isShuffled && playbackSettings.keepShuffle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ import org.oxycblt.auxio.list.Header
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.ListFragment
|
import org.oxycblt.auxio.list.ListFragment
|
||||||
import org.oxycblt.auxio.list.ListViewModel
|
import org.oxycblt.auxio.list.ListViewModel
|
||||||
import org.oxycblt.auxio.list.Menu
|
import org.oxycblt.auxio.list.menu.Menu
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
|
@ -174,7 +174,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
||||||
|
|
||||||
override fun onRealClick(item: Music) {
|
override fun onRealClick(item: Music) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> playbackModel.playFrom(item, searchModel.playbackMode)
|
is Song -> playbackModel.play(item, searchModel.playWith)
|
||||||
is Album -> detailModel.showAlbum(item)
|
is Album -> detailModel.showAlbum(item)
|
||||||
is Artist -> detailModel.showArtist(item)
|
is Artist -> detailModel.showArtist(item)
|
||||||
is Genre -> detailModel.showGenre(item)
|
is Genre -> detailModel.showGenre(item)
|
||||||
|
|
@ -182,9 +182,9 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Music, anchor: View) {
|
override fun onOpenMenu(item: Music) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> listModel.openMenu(R.menu.item_song, item)
|
is Song -> listModel.openMenu(R.menu.item_song, item, searchModel.playWith)
|
||||||
is Album -> listModel.openMenu(R.menu.item_album, item)
|
is Album -> listModel.openMenu(R.menu.item_album, item)
|
||||||
is Artist -> listModel.openMenu(R.menu.item_parent, item)
|
is Artist -> listModel.openMenu(R.menu.item_parent, item)
|
||||||
is Genre -> listModel.openMenu(R.menu.item_parent, item)
|
is Genre -> listModel.openMenu(R.menu.item_parent, item)
|
||||||
|
|
@ -256,16 +256,11 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
||||||
if (menu == null) return
|
if (menu == null) return
|
||||||
val directions =
|
val directions =
|
||||||
when (menu) {
|
when (menu) {
|
||||||
is Menu.ForSong ->
|
is Menu.ForSong -> SearchFragmentDirections.openSongMenu(menu.parcel)
|
||||||
SearchFragmentDirections.openSongMenu(menu.menuRes, menu.music.uid)
|
is Menu.ForAlbum -> SearchFragmentDirections.openAlbumMenu(menu.parcel)
|
||||||
is Menu.ForAlbum ->
|
is Menu.ForArtist -> SearchFragmentDirections.openArtistMenu(menu.parcel)
|
||||||
SearchFragmentDirections.openAlbumMenu(menu.menuRes, menu.music.uid)
|
is Menu.ForGenre -> SearchFragmentDirections.openGenreMenu(menu.parcel)
|
||||||
is Menu.ForArtist ->
|
is Menu.ForPlaylist -> SearchFragmentDirections.openPlaylistMenu(menu.parcel)
|
||||||
SearchFragmentDirections.openArtistMenu(menu.menuRes, menu.music.uid)
|
|
||||||
is Menu.ForGenre ->
|
|
||||||
SearchFragmentDirections.openGenreMenu(menu.menuRes, menu.music.uid)
|
|
||||||
is Menu.ForPlaylist ->
|
|
||||||
SearchFragmentDirections.openPlaylistMenu(menu.menuRes, menu.music.uid)
|
|
||||||
}
|
}
|
||||||
findNavController().navigateSafe(directions)
|
findNavController().navigateSafe(directions)
|
||||||
// Keyboard is no longer needed.
|
// Keyboard is no longer needed.
|
||||||
|
|
|
||||||
|
|
@ -27,5 +27,6 @@ import dagger.hilt.components.SingletonComponent
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface SearchModule {
|
interface SearchModule {
|
||||||
@Binds fun engine(searchEngine: SearchEngineImpl): SearchEngine
|
@Binds fun engine(searchEngine: SearchEngineImpl): SearchEngine
|
||||||
|
|
||||||
@Binds fun settings(searchSettings: SearchSettingsImpl): SearchSettings
|
@Binds fun settings(searchSettings: SearchSettingsImpl): SearchSettings
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import androidx.core.content.edit
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicType
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -32,19 +32,21 @@ import org.oxycblt.auxio.settings.Settings
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
interface SearchSettings : Settings<Nothing> {
|
interface SearchSettings : Settings<Nothing> {
|
||||||
/** The type of Music the search view is currently filtering to. */
|
/** The type of Music the search view is should filter to. */
|
||||||
var searchFilterMode: MusicMode?
|
var filterTo: MusicType?
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchSettingsImpl @Inject constructor(@ApplicationContext context: Context) :
|
class SearchSettingsImpl @Inject constructor(@ApplicationContext context: Context) :
|
||||||
Settings.Impl<Nothing>(context), SearchSettings {
|
Settings.Impl<Nothing>(context), SearchSettings {
|
||||||
override var searchFilterMode: MusicMode?
|
override var filterTo: MusicType?
|
||||||
get() =
|
get() =
|
||||||
MusicMode.fromIntCode(
|
MusicType.fromIntCode(
|
||||||
sharedPreferences.getInt(getString(R.string.set_key_search_filter), Int.MIN_VALUE))
|
sharedPreferences.getInt(
|
||||||
|
getString(R.string.set_key_search_filter_to), Int.MIN_VALUE))
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
putInt(getString(R.string.set_key_search_filter), value?.intCode ?: Int.MIN_VALUE)
|
putInt(
|
||||||
|
getString(R.string.set_key_search_filter_to), value?.intCode ?: Int.MIN_VALUE)
|
||||||
apply()
|
apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,13 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.list.BasicHeader
|
import org.oxycblt.auxio.list.BasicHeader
|
||||||
import org.oxycblt.auxio.list.Divider
|
import org.oxycblt.auxio.list.Divider
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.Sort
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
|
import org.oxycblt.auxio.music.MusicType
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.device.DeviceLibrary
|
import org.oxycblt.auxio.music.device.DeviceLibrary
|
||||||
import org.oxycblt.auxio.music.user.UserLibrary
|
import org.oxycblt.auxio.music.user.UserLibrary
|
||||||
|
import org.oxycblt.auxio.playback.PlaySong
|
||||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
|
@ -63,9 +64,9 @@ constructor(
|
||||||
val searchResults: StateFlow<List<Item>>
|
val searchResults: StateFlow<List<Item>>
|
||||||
get() = _searchResults
|
get() = _searchResults
|
||||||
|
|
||||||
/** The [MusicMode] to use when playing a [Song] from the UI. */
|
/** The [PlaySong] instructions to use when playing a [Song]. */
|
||||||
val playbackMode: MusicMode
|
val playWith
|
||||||
get() = playbackSettings.inListPlaybackMode
|
get() = playbackSettings.playInListWith
|
||||||
|
|
||||||
init {
|
init {
|
||||||
musicRepository.addUpdateListener(this)
|
musicRepository.addUpdateListener(this)
|
||||||
|
|
@ -116,12 +117,12 @@ constructor(
|
||||||
userLibrary: UserLibrary,
|
userLibrary: UserLibrary,
|
||||||
query: String
|
query: String
|
||||||
): List<Item> {
|
): List<Item> {
|
||||||
val filterMode = searchSettings.searchFilterMode
|
val filter = searchSettings.filterTo
|
||||||
|
|
||||||
val items =
|
val items =
|
||||||
if (filterMode == null) {
|
if (filter == null) {
|
||||||
// A nulled filter mode means to not filter anything.
|
// A nulled filter type means to not filter anything.
|
||||||
logD("No filter mode specified, using entire library")
|
logD("No filter specified, using entire library")
|
||||||
SearchEngine.Items(
|
SearchEngine.Items(
|
||||||
deviceLibrary.songs,
|
deviceLibrary.songs,
|
||||||
deviceLibrary.albums,
|
deviceLibrary.albums,
|
||||||
|
|
@ -129,14 +130,13 @@ constructor(
|
||||||
deviceLibrary.genres,
|
deviceLibrary.genres,
|
||||||
userLibrary.playlists)
|
userLibrary.playlists)
|
||||||
} else {
|
} else {
|
||||||
logD("Filter mode specified, filtering library")
|
logD("Filter specified, reducing library")
|
||||||
SearchEngine.Items(
|
SearchEngine.Items(
|
||||||
songs = if (filterMode == MusicMode.SONGS) deviceLibrary.songs else null,
|
songs = if (filter == MusicType.SONGS) deviceLibrary.songs else null,
|
||||||
albums = if (filterMode == MusicMode.ALBUMS) deviceLibrary.albums else null,
|
albums = if (filter == MusicType.ALBUMS) deviceLibrary.albums else null,
|
||||||
artists = if (filterMode == MusicMode.ARTISTS) deviceLibrary.artists else null,
|
artists = if (filter == MusicType.ARTISTS) deviceLibrary.artists else null,
|
||||||
genres = if (filterMode == MusicMode.GENRES) deviceLibrary.genres else null,
|
genres = if (filter == MusicType.GENRES) deviceLibrary.genres else null,
|
||||||
playlists =
|
playlists = if (filter == MusicType.PLAYLISTS) userLibrary.playlists else null)
|
||||||
if (filterMode == MusicMode.PLAYLISTS) userLibrary.playlists else null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val results = searchEngine.search(items, query)
|
val results = searchEngine.search(items, query)
|
||||||
|
|
@ -198,35 +198,35 @@ constructor(
|
||||||
*/
|
*/
|
||||||
@IdRes
|
@IdRes
|
||||||
fun getFilterOptionId() =
|
fun getFilterOptionId() =
|
||||||
when (searchSettings.searchFilterMode) {
|
when (searchSettings.filterTo) {
|
||||||
MusicMode.SONGS -> R.id.option_filter_songs
|
MusicType.SONGS -> R.id.option_filter_songs
|
||||||
MusicMode.ALBUMS -> R.id.option_filter_albums
|
MusicType.ALBUMS -> R.id.option_filter_albums
|
||||||
MusicMode.ARTISTS -> R.id.option_filter_artists
|
MusicType.ARTISTS -> R.id.option_filter_artists
|
||||||
MusicMode.GENRES -> R.id.option_filter_genres
|
MusicType.GENRES -> R.id.option_filter_genres
|
||||||
MusicMode.PLAYLISTS -> R.id.option_filter_playlists
|
MusicType.PLAYLISTS -> R.id.option_filter_playlists
|
||||||
// Null maps to filtering nothing.
|
// Null maps to filtering nothing.
|
||||||
null -> R.id.option_filter_all
|
null -> R.id.option_filter_all
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the filter mode with the newly-selected filter option.
|
* Update the filter type with the newly-selected filter option.
|
||||||
*
|
*
|
||||||
* @return A menu item ID of the new filtering option selected.
|
* @return A menu item ID of the new filtering option selected.
|
||||||
*/
|
*/
|
||||||
fun setFilterOptionId(@IdRes id: Int) {
|
fun setFilterOptionId(@IdRes id: Int) {
|
||||||
val newFilterMode =
|
val newFilter =
|
||||||
when (id) {
|
when (id) {
|
||||||
R.id.option_filter_songs -> MusicMode.SONGS
|
R.id.option_filter_songs -> MusicType.SONGS
|
||||||
R.id.option_filter_albums -> MusicMode.ALBUMS
|
R.id.option_filter_albums -> MusicType.ALBUMS
|
||||||
R.id.option_filter_artists -> MusicMode.ARTISTS
|
R.id.option_filter_artists -> MusicType.ARTISTS
|
||||||
R.id.option_filter_genres -> MusicMode.GENRES
|
R.id.option_filter_genres -> MusicType.GENRES
|
||||||
R.id.option_filter_playlists -> MusicMode.PLAYLISTS
|
R.id.option_filter_playlists -> MusicType.PLAYLISTS
|
||||||
// Null maps to filtering nothing.
|
// Null maps to filtering nothing.
|
||||||
R.id.option_filter_all -> null
|
R.id.option_filter_all -> null
|
||||||
else -> error("Invalid option ID provided")
|
else -> error("Invalid option ID provided")
|
||||||
}
|
}
|
||||||
logD("Updating filter mode to $newFilterMode")
|
logD("Updating filter type to $newFilter")
|
||||||
searchSettings.searchFilterMode = newFilterMode
|
searchSettings.filterTo = newFilter
|
||||||
search(lastQuery)
|
search(lastQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,6 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
|
||||||
// case, we will try to manually handle these cases before we try to launch the
|
// case, we will try to manually handle these cases before we try to launch the
|
||||||
// browser.
|
// browser.
|
||||||
logD("Resolving browser activity for chooser")
|
logD("Resolving browser activity for chooser")
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val pkgName =
|
val pkgName =
|
||||||
context.packageManager
|
context.packageManager
|
||||||
.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
|
.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import org.oxycblt.auxio.util.fixDoubleRipple
|
||||||
class IntListPreferenceDialog : PreferenceDialogFragmentCompat() {
|
class IntListPreferenceDialog : PreferenceDialogFragmentCompat() {
|
||||||
private val listPreference: IntListPreference
|
private val listPreference: IntListPreference
|
||||||
get() = (preference as IntListPreference)
|
get() = (preference as IntListPreference)
|
||||||
|
|
||||||
private var pendingValueIndex = -1
|
private var pendingValueIndex = -1
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ abstract class BaseBottomSheetBehavior<V : View>(context: Context, attributeSet:
|
||||||
|
|
||||||
// Enable experimental settings that allow us to skip the half-expanded state.
|
// Enable experimental settings that allow us to skip the half-expanded state.
|
||||||
override fun shouldSkipHalfExpandedStateWhenDragging() = true
|
override fun shouldSkipHalfExpandedStateWhenDragging() = true
|
||||||
|
|
||||||
override fun shouldExpandOnUpwardDrag(dragDurationMillis: Long, yPositionPercentage: Float) =
|
override fun shouldExpandOnUpwardDrag(dragDurationMillis: Long, yPositionPercentage: Float) =
|
||||||
true
|
true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,16 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.ui
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.annotation.StyleRes
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
@ -39,13 +42,8 @@ abstract class ViewBindingBottomSheetDialogFragment<VB : ViewBinding> :
|
||||||
BottomSheetDialogFragment() {
|
BottomSheetDialogFragment() {
|
||||||
private var _binding: VB? = null
|
private var _binding: VB? = null
|
||||||
|
|
||||||
/**
|
override fun onCreateDialog(savedInstanceState: Bundle?): BottomSheetDialog =
|
||||||
* Configure the [AlertDialog.Builder] during [onCreateDialog].
|
TweakedBottomSheetDialog(requireContext(), theme)
|
||||||
*
|
|
||||||
* @param builder The [AlertDialog.Builder] to configure.
|
|
||||||
* @see onCreateDialog
|
|
||||||
*/
|
|
||||||
protected open fun onConfigDialog(builder: AlertDialog.Builder) {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inflate the [ViewBinding] during [onCreateView].
|
* Inflate the [ViewBinding] during [onCreateView].
|
||||||
|
|
@ -108,4 +106,22 @@ abstract class ViewBindingBottomSheetDialogFragment<VB : ViewBinding> :
|
||||||
_binding = null
|
_binding = null
|
||||||
logD("Fragment destroyed")
|
logD("Fragment destroyed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class TweakedBottomSheetDialog
|
||||||
|
@JvmOverloads
|
||||||
|
constructor(context: Context, @StyleRes theme: Int = 0) : BottomSheetDialog(context, theme) {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
// Collapsed state is bugged in phone landscape mode and shows only 10% of the dialog.
|
||||||
|
// Just disable it and go directly from expanded -> hidden.
|
||||||
|
behavior.skipCollapsed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
// Manually trigger an expanded transition to make window insets actually apply to
|
||||||
|
// the dialog on the first layout pass. I don't know why this works.
|
||||||
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import android.os.Build
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
private val ACCENT_NAMES =
|
private val accentNames =
|
||||||
intArrayOf(
|
intArrayOf(
|
||||||
R.string.clr_red,
|
R.string.clr_red,
|
||||||
R.string.clr_pink,
|
R.string.clr_pink,
|
||||||
|
|
@ -42,7 +42,7 @@ private val ACCENT_NAMES =
|
||||||
R.string.clr_grey,
|
R.string.clr_grey,
|
||||||
R.string.clr_dynamic)
|
R.string.clr_dynamic)
|
||||||
|
|
||||||
private val ACCENT_THEMES =
|
private val accentThemes =
|
||||||
intArrayOf(
|
intArrayOf(
|
||||||
R.style.Theme_Auxio_Red,
|
R.style.Theme_Auxio_Red,
|
||||||
R.style.Theme_Auxio_Pink,
|
R.style.Theme_Auxio_Pink,
|
||||||
|
|
@ -63,7 +63,7 @@ private val ACCENT_THEMES =
|
||||||
R.style.Theme_Auxio_App // Dynamic colors are on the base theme
|
R.style.Theme_Auxio_App // Dynamic colors are on the base theme
|
||||||
)
|
)
|
||||||
|
|
||||||
private val ACCENT_BLACK_THEMES =
|
private val accentBlackThemes =
|
||||||
intArrayOf(
|
intArrayOf(
|
||||||
R.style.Theme_Auxio_Black_Red,
|
R.style.Theme_Auxio_Black_Red,
|
||||||
R.style.Theme_Auxio_Black_Pink,
|
R.style.Theme_Auxio_Black_Pink,
|
||||||
|
|
@ -84,7 +84,7 @@ private val ACCENT_BLACK_THEMES =
|
||||||
R.style.Theme_Auxio_Black // Dynamic colors are on the base theme
|
R.style.Theme_Auxio_Black // Dynamic colors are on the base theme
|
||||||
)
|
)
|
||||||
|
|
||||||
private val ACCENT_PRIMARY_COLORS =
|
private val accentPrimaryColors =
|
||||||
intArrayOf(
|
intArrayOf(
|
||||||
R.color.red_primary,
|
R.color.red_primary,
|
||||||
R.color.pink_primary,
|
R.color.pink_primary,
|
||||||
|
|
@ -115,18 +115,18 @@ private val ACCENT_PRIMARY_COLORS =
|
||||||
class Accent private constructor(val index: Int) {
|
class Accent private constructor(val index: Int) {
|
||||||
/** The name of this [Accent]. */
|
/** The name of this [Accent]. */
|
||||||
val name: Int
|
val name: Int
|
||||||
get() = ACCENT_NAMES[index]
|
get() = accentNames[index]
|
||||||
/** The theme resource for this accent. */
|
/** The theme resource for this accent. */
|
||||||
val theme: Int
|
val theme: Int
|
||||||
get() = ACCENT_THEMES[index]
|
get() = accentThemes[index]
|
||||||
/**
|
/**
|
||||||
* The black theme resource for this accent. Identical to [theme], but with a black background.
|
* The black theme resource for this accent. Identical to [theme], but with a black background.
|
||||||
*/
|
*/
|
||||||
val blackTheme: Int
|
val blackTheme: Int
|
||||||
get() = ACCENT_BLACK_THEMES[index]
|
get() = accentBlackThemes[index]
|
||||||
/** The accent's primary color. */
|
/** The accent's primary color. */
|
||||||
val primary: Int
|
val primary: Int
|
||||||
get() = ACCENT_PRIMARY_COLORS[index]
|
get() = accentPrimaryColors[index]
|
||||||
|
|
||||||
override fun equals(other: Any?) = other is Accent && index == other.index
|
override fun equals(other: Any?) = other is Accent && index == other.index
|
||||||
|
|
||||||
|
|
@ -152,7 +152,7 @@ class Accent private constructor(val index: Int) {
|
||||||
val DEFAULT =
|
val DEFAULT =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
// Use dynamic coloring on devices that support it.
|
// Use dynamic coloring on devices that support it.
|
||||||
ACCENT_THEMES.lastIndex
|
accentThemes.lastIndex
|
||||||
} else {
|
} else {
|
||||||
// Use blue everywhere else.
|
// Use blue everywhere else.
|
||||||
5
|
5
|
||||||
|
|
@ -161,10 +161,10 @@ class Accent private constructor(val index: Int) {
|
||||||
/** The amount of valid accents. */
|
/** The amount of valid accents. */
|
||||||
val MAX =
|
val MAX =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
ACCENT_THEMES.size
|
accentThemes.size
|
||||||
} else {
|
} else {
|
||||||
// Disable the option for a dynamic accent on unsupported devices.
|
// Disable the option for a dynamic accent on unsupported devices.
|
||||||
ACCENT_THEMES.size - 1
|
accentThemes.size - 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue