diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 76abe25e7..de42bfe09 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -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 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. -- Stick to [F-Droid Including 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. +- Stick to [F-Droid Inclusion Guidelines](https://f-droid.org/wiki/page/Inclusion_Policy) +- 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. -- 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index ca2830c2b..e9e66ee13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,13 +3,18 @@ ## dev #### 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 - Made "Add to Playlist" action more prominent in selection toolbar - Fixed notification album covers not updating after changing the cover aspect ratio setting +#### What's Fixed +- Playlist detail view now respects playback settings + #### Dev/Meta - Unified navigation graph diff --git a/README.md b/README.md index 012eae415..a324ffd3c 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ ## About -Auxio is a local music player with a fast, reliable UI/UX without the many useless features present in other music players. Built off of [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. @@ -42,7 +42,7 @@ I primarily built Auxio for myself, but you can use it too, I guess. ## 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 - Opinionated UX that prioritizes ease of use over edge cases - Customizable behavior @@ -69,12 +69,11 @@ precise/original dates, sort tags, and more ## Building -Auxio relies on a custom version of ExoPlayer that enables some extra features. This adds some caveats to -the build process: +Auxio relies on a custom version of Media3 that enables some extra features. This adds some caveats to the build process: 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 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. ## Contributing diff --git a/app/build.gradle b/app/build.gradle index 0ec8807b2..f088245c7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,8 @@ plugins { id "kotlin-parcelize" id "dagger.hilt.android.plugin" id "kotlin-kapt" - id 'org.jetbrains.kotlin.android' + id "com.google.devtools.ksp" + id "org.jetbrains.kotlin.android" } android { @@ -77,7 +78,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$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-guava:$coroutines_version" @@ -118,7 +119,9 @@ dependencies { // Database def room_version = '2.6.0-alpha02' 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" // --- THIRD PARTY --- @@ -142,7 +145,7 @@ dependencies { kapt "com.google.dagger:hilt-android-compiler:$hilt_version" // Testing - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.11' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12' testImplementation "junit:junit:4.13.2" androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' diff --git a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt index d0bff5315..54d59eb50 100644 --- a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt +++ b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt @@ -65,14 +65,14 @@ object IntegerTable { const val REPEAT_MODE_ALL = 0xA101 /** RepeatMode.TRACK */ const val REPEAT_MODE_TRACK = 0xA102 - /** PlaybackMode.IN_GENRE */ - const val PLAYBACK_MODE_IN_GENRE = 0xA103 - /** PlaybackMode.IN_ARTIST */ - const val PLAYBACK_MODE_IN_ARTIST = 0xA104 - /** PlaybackMode.IN_ALBUM */ - const val PLAYBACK_MODE_IN_ALBUM = 0xA105 - /** PlaybackMode.ALL_SONGS */ - const val PLAYBACK_MODE_ALL_SONGS = 0xA106 + // /** PlaybackMode.IN_GENRE (No longer used but still reserved) */ + // const val PLAYBACK_MODE_IN_GENRE = 0xA103 + // /** PlaybackMode.IN_ARTIST (No longer used but still reserved) */ + // const val PLAYBACK_MODE_IN_ARTIST = 0xA104 + // /** PlaybackMode.IN_ALBUM (No longer used but still reserved) */ + // const val PLAYBACK_MODE_IN_ALBUM = 0xA105 + // /** PlaybackMode.ALL_SONGS (No longer used but still reserved) */ + // const val PLAYBACK_MODE_ALL_SONGS = 0xA106 /** MusicMode.SONGS */ const val MUSIC_MODE_SONGS = 0xA10B /** MusicMode.ALBUMS */ @@ -101,8 +101,6 @@ object IntegerTable { const val SORT_BY_TRACK = 0xA117 /** Sort.Mode.ByDateAdded */ const val SORT_BY_DATE_ADDED = 0xA118 - /** Sort.Mode.None */ - const val SORT_BY_NONE = 0xA11F /** ReplayGainMode.Off (No longer used but still reserved) */ // const val REPLAY_GAIN_MODE_OFF = 0xA110 /** ReplayGainMode.Track */ @@ -123,4 +121,16 @@ object IntegerTable { const val COVER_MODE_MEDIA_STORE = 0xA11D /** CoverMode.Quality */ 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 } diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 48e38cf5d..2d510ea36 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -26,19 +26,22 @@ import androidx.activity.OnBackPressedCallback import androidx.core.view.ViewCompat import androidx.core.view.isInvisible import androidx.core.view.updatePadding -import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.activityViewModels import androidx.navigation.NavController import androidx.navigation.NavDestination import androidx.navigation.findNavController +import androidx.navigation.fragment.findNavController import com.google.android.material.R as MR import com.google.android.material.bottomsheet.BackportBottomSheetBehavior import com.google.android.material.shape.MaterialShapeDrawable +import com.google.android.material.transition.MaterialFadeThrough import dagger.hilt.android.AndroidEntryPoint import kotlin.math.max import kotlin.math.min import org.oxycblt.auxio.databinding.FragmentMainBinding 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.music.Music 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.getDimen import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.unlikelyToBeNull @@ -66,17 +70,23 @@ class MainFragment : ViewBindingFragment(), ViewTreeObserver.OnPreDrawListener, NavController.OnDestinationChangedListener { - private val playbackModel: PlaybackViewModel by activityViewModels() - private val listModel: ListViewModel 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 detailBackCallback: DetailBackPressedCallback? = null private var selectionBackCallback: SelectionBackPressedCallback? = null - private var exploreBackCallback: ExploreBackPressedCallback? = null private var lastInsets: WindowInsets? = null private var elevationNormal = 0f 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 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 // that instantiating these callbacks in their respective fragments would result in the // correct order. - val sheetBackCallback = + sheetBackCallback = SheetBackPressedCallback( - playbackSheetBehavior = playbackSheetBehavior, - queueSheetBehavior = queueSheetBehavior) - .also { sheetBackCallback = it } + playbackSheetBehavior = playbackSheetBehavior, + queueSheetBehavior = queueSheetBehavior) val detailBackCallback = DetailBackPressedCallback(detailModel).also { detailBackCallback = it } val selectionBackCallback = SelectionBackPressedCallback(listModel).also { selectionBackCallback = it } - val exploreBackCallback = - ExploreBackPressedCallback(binding.exploreNavHost).also { exploreBackCallback = it } // --- UI SETUP --- 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 -> lastInsets = insets @@ -152,6 +151,7 @@ class MainFragment : // --- VIEWMODEL SETUP --- collectImmediately(detailModel.editedPlaylist, detailBackCallback::invalidateEnabled) + collectImmediately(homeModel.showOuter.flow, ::handleShowOuter) collectImmediately(listModel.selected, selectionBackCallback::invalidateEnabled) collectImmediately(playbackModel.song, ::updateSong) collectImmediately(playbackModel.openPanel.flow, ::handlePanel) @@ -169,6 +169,18 @@ class 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() { super.onStop() val binding = requireBinding() @@ -181,7 +193,6 @@ class MainFragment : sheetBackCallback = null detailBackCallback = null selectionBackCallback = null - exploreBackCallback = null } override fun onPreDraw(): Boolean { @@ -283,8 +294,6 @@ class MainFragment : // 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 // rotates. - requireNotNull(exploreBackCallback) { "ExploreBackPressedCallback was not available" } - .invalidateEnabled() if (!initialNavDestinationChange) { initialNavDestinationChange = true return @@ -292,6 +301,17 @@ class MainFragment : 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?) { if (song != null) { tryShowSheets() @@ -462,23 +482,4 @@ class MainFragment : 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 - } - } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index b654902da..7227d9aa6 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -20,8 +20,6 @@ package org.oxycblt.auxio.detail import android.os.Bundle import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController 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.ListFragment import org.oxycblt.auxio.list.ListViewModel -import org.oxycblt.auxio.list.Menu -import org.oxycblt.auxio.list.Sort +import org.oxycblt.auxio.list.menu.Menu import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel 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.collectImmediately import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.navigateSafe +import org.oxycblt.auxio.util.overrideOnOverflowMenuClick import org.oxycblt.auxio.util.setFullWidthLookup -import org.oxycblt.auxio.util.share -import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull /** @@ -77,6 +71,7 @@ class AlbumDetailFragment : override val listModel: ListViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() + // 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. private val args: AlbumDetailFragmentArgs by navArgs() @@ -103,16 +98,18 @@ class AlbumDetailFragment : // --- UI SETUP -- binding.detailNormalToolbar.apply { - inflateMenu(R.menu.toolbar_album) setNavigationOnClickListener { findNavController().navigateUp() } - setOnMenuItemClickListener(this@AlbumDetailFragment) + overrideOnOverflowMenuClick { + listModel.openMenu( + R.menu.item_detail_album, unlikelyToBeNull(detailModel.currentAlbum.value)) + } } binding.detailRecycler.apply { adapter = ConcatAdapter(albumHeaderAdapter, albumListAdapter) (layoutManager as GridLayoutManager).setFullWidthLookup { 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 } else { true @@ -124,7 +121,7 @@ class AlbumDetailFragment : // DetailViewModel handles most initialization from the navigation argument. detailModel.setAlbum(args.albumUid) collectImmediately(detailModel.currentAlbum, ::updateAlbum) - collectImmediately(detailModel.albumList, ::updateList) + collectImmediately(detailModel.albumSongList, ::updateList) collect(detailModel.toShow.flow, ::handleShow) collect(listModel.menu.flow, ::handleMenu) collectImmediately(listModel.selected, ::updateSelection) @@ -140,52 +137,15 @@ class AlbumDetailFragment : binding.detailRecycler.adapter = null // 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. - detailModel.albumInstructions.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 - } - } + detailModel.albumSongInstructions.consume() } override fun onRealClick(item: Song) { - // There can only be one album, so a null mode and an ALBUMS mode will function the same. - playbackModel.playFrom(item, detailModel.playbackMode ?: MusicMode.ALBUMS) + playbackModel.play(item, detailModel.playInAlbumWith) } - override fun onOpenMenu(item: Song, anchor: View) { - listModel.openMenu(R.menu.item_album_song, item) + override fun onOpenMenu(item: Song) { + listModel.openMenu(R.menu.item_album_song, item, detailModel.playInAlbumWith) } override fun onPlay() { @@ -196,31 +156,8 @@ class AlbumDetailFragment : playbackModel.shuffle(unlikelyToBeNull(detailModel.currentAlbum.value)) } - override fun onOpenSortMenu(anchor: View) { - openMenu(anchor, R.menu.sort_album) { - // 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 onOpenSortMenu() { + findNavController().navigateSafe(AlbumDetailFragmentDirections.sort()) } override fun onNavigateToParentArtist() { @@ -238,7 +175,7 @@ class AlbumDetailFragment : } private fun updateList(list: List) { - albumListAdapter.update(list, detailModel.albumInstructions.consume()) + albumListAdapter.update(list, detailModel.albumSongInstructions.consume()) } private fun handleShow(show: Show?) { @@ -304,9 +241,8 @@ class AlbumDetailFragment : if (menu == null) return val directions = when (menu) { - is Menu.ForSong -> - AlbumDetailFragmentDirections.openSongMenu(menu.menuRes, menu.music.uid) - is Menu.ForAlbum, + is Menu.ForSong -> AlbumDetailFragmentDirections.openSongMenu(menu.parcel) + is Menu.ForAlbum -> AlbumDetailFragmentDirections.openAlbumMenu(menu.parcel) is Menu.ForArtist, is Menu.ForGenre, is Menu.ForPlaylist -> error("Unexpected menu $menu") @@ -365,7 +301,7 @@ class AlbumDetailFragment : private fun scrollToAlbumSong(song: Song) { // 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) { // Only scroll if the song is within this album. diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 6eebd76dd..c209a1a05 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -20,8 +20,6 @@ package org.oxycblt.auxio.detail import android.os.Bundle import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController 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.ListFragment import org.oxycblt.auxio.list.ListViewModel -import org.oxycblt.auxio.list.Menu -import org.oxycblt.auxio.list.Sort +import org.oxycblt.auxio.list.menu.Menu import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist 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.collectImmediately import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.navigateSafe +import org.oxycblt.auxio.util.overrideOnOverflowMenuClick import org.oxycblt.auxio.util.setFullWidthLookup -import org.oxycblt.auxio.util.share -import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull /** @@ -101,9 +96,12 @@ class ArtistDetailFragment : // --- UI SETUP --- binding.detailNormalToolbar.apply { - inflateMenu(R.menu.toolbar_parent) setNavigationOnClickListener { findNavController().navigateUp() } setOnMenuItemClickListener(this@ArtistDetailFragment) + overrideOnOverflowMenuClick { + listModel.openMenu( + R.menu.item_detail_parent, unlikelyToBeNull(detailModel.currentArtist.value)) + } } binding.detailRecycler.apply { @@ -111,7 +109,7 @@ class ArtistDetailFragment : (layoutManager as GridLayoutManager).setFullWidthLookup { if (it != 0) { val item = - detailModel.artistList.value.getOrElse(it - 1) { + detailModel.artistSongList.value.getOrElse(it - 1) { return@setFullWidthLookup false } item is Divider || item is Header @@ -125,7 +123,7 @@ class ArtistDetailFragment : // DetailViewModel handles most initialization from the navigation argument. detailModel.setArtist(args.artistUid) collectImmediately(detailModel.currentArtist, ::updateArtist) - collectImmediately(detailModel.artistList, ::updateList) + collectImmediately(detailModel.artistSongList, ::updateList) collect(detailModel.toShow.flow, ::handleShow) collect(listModel.menu.flow, ::handleMenu) collectImmediately(listModel.selected, ::updateSelection) @@ -141,62 +139,21 @@ class ArtistDetailFragment : binding.detailRecycler.adapter = null // 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. - detailModel.artistInstructions.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 - } - } + detailModel.artistSongInstructions.consume() } override fun onRealClick(item: Music) { when (item) { is Album -> detailModel.showAlbum(item) - is Song -> { - 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)) - } - } + is Song -> playbackModel.play(item, detailModel.playInArtistWith) else -> error("Unexpected datatype: ${item::class.simpleName}") } } - override fun onOpenMenu(item: Music, anchor: View) { + override fun onOpenMenu(item: Music) { 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) else -> error("Unexpected datatype: ${item::class.simpleName}") } @@ -210,33 +167,8 @@ class ArtistDetailFragment : playbackModel.shuffle(unlikelyToBeNull(detailModel.currentArtist.value)) } - override fun onOpenSortMenu(anchor: View) { - openMenu(anchor, R.menu.sort_artist) { - // 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 - } - } + override fun onOpenSortMenu() { + findNavController().navigateSafe(ArtistDetailFragmentDirections.sort()) } private fun updateArtist(artist: Artist?) { @@ -245,24 +177,12 @@ class ArtistDetailFragment : findNavController().navigateUp() return } - requireBinding().detailNormalToolbar.apply { - 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 - } + requireBinding().detailNormalToolbar.title = artist.name.resolve(requireContext()) artistHeaderAdapter.setParent(artist) } private fun updateList(list: List) { - artistListAdapter.update(list, detailModel.artistInstructions.consume()) + artistListAdapter.update(list, detailModel.artistSongInstructions.consume()) } private fun handleShow(show: Show?) { @@ -316,11 +236,9 @@ class ArtistDetailFragment : if (menu == null) return val directions = when (menu) { - is Menu.ForSong -> - ArtistDetailFragmentDirections.openSongMenu(menu.menuRes, menu.music.uid) - is Menu.ForAlbum -> - ArtistDetailFragmentDirections.openAlbumMenu(menu.menuRes, menu.music.uid) - is Menu.ForArtist, + is Menu.ForSong -> ArtistDetailFragmentDirections.openSongMenu(menu.parcel) + is Menu.ForAlbum -> ArtistDetailFragmentDirections.openAlbumMenu(menu.parcel) + is Menu.ForArtist -> ArtistDetailFragmentDirections.openArtistMenu(menu.parcel) is Menu.ForGenre, is Menu.ForPlaylist -> error("Unexpected menu $menu") } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt index 28c1f65f7..3c494cd96 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt @@ -59,7 +59,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr override fun onAttachedToWindow() { super.onAttachedToWindow() - (layoutParams as CoordinatorLayout.LayoutParams).behavior = Behavior(context) + if (!isInEditMode) { + (layoutParams as CoordinatorLayout.LayoutParams).behavior = Behavior(context) + } } private fun findTitleView(): TextView { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index ee1fe4df0..648424458 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -36,24 +36,25 @@ import org.oxycblt.auxio.detail.list.SortHeader import org.oxycblt.auxio.list.BasicHeader import org.oxycblt.auxio.list.Divider 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.sort.Sort 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.MusicMode import org.oxycblt.auxio.music.MusicRepository -import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.music.metadata.AudioProperties +import org.oxycblt.auxio.playback.PlaySong import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent import org.oxycblt.auxio.util.logD 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 @@ -65,9 +66,9 @@ import org.oxycblt.auxio.util.logW class DetailViewModel @Inject constructor( + private val listSettings: ListSettings, private val musicRepository: MusicRepository, private val audioPropertiesFactory: AudioProperties.Factory, - private val musicSettings: MusicSettings, private val playbackSettings: PlaybackSettings ) : ViewModel(), MusicRepository.UpdateListener { private val _toShow = MutableEvent() @@ -97,23 +98,23 @@ constructor( val currentAlbum: StateFlow get() = _currentAlbum - private val _albumList = MutableStateFlow(listOf()) + private val _albumSongList = MutableStateFlow(listOf()) /** The current list data derived from [currentAlbum]. */ - val albumList: StateFlow> - get() = _albumList - private val _albumInstructions = MutableEvent() - /** Instructions for updating [albumList] in the UI. */ - val albumInstructions: Event - get() = _albumInstructions + val albumSongList: StateFlow> + get() = _albumSongList - /** The current [Sort] used for [Song]s in [albumList]. */ - var albumSongSort: Sort - get() = musicSettings.albumSongSort - set(value) { - musicSettings.albumSongSort = value - // Refresh the album list to reflect the new sort. - currentAlbum.value?.let { refreshAlbumList(it, true) } - } + private val _albumSongInstructions = MutableEvent() + /** Instructions for updating [albumSongList] in the UI. */ + val albumSongInstructions: Event + get() = _albumSongInstructions + + /** The current [Sort] used for [Song]s in [albumSongList]. */ + 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 --- @@ -122,23 +123,28 @@ constructor( val currentArtist: StateFlow get() = _currentArtist - private val _artistList = MutableStateFlow(listOf()) + private val _artistSongList = MutableStateFlow(listOf()) /** The current list derived from [currentArtist]. */ - val artistList: StateFlow> = _artistList - private val _artistInstructions = MutableEvent() - /** Instructions for updating [artistList] in the UI. */ - val artistInstructions: Event - get() = _artistInstructions + val artistSongList: StateFlow> = _artistSongList - /** The current [Sort] used for [Song]s in [artistList]. */ + private val _artistSongInstructions = MutableEvent() + /** Instructions for updating [artistSongList] in the UI. */ + val artistSongInstructions: Event + get() = _artistSongInstructions + + /** The current [Sort] used for [Song]s in [artistSongList]. */ var artistSongSort: Sort - get() = musicSettings.artistSongSort + get() = listSettings.artistSongSort set(value) { - musicSettings.artistSongSort = value + listSettings.artistSongSort = value // Refresh the artist list to reflect the new sort. 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 --- private val _currentGenre = MutableStateFlow(null) @@ -146,23 +152,28 @@ constructor( val currentGenre: StateFlow get() = _currentGenre - private val _genreList = MutableStateFlow(listOf()) + private val _genreSongList = MutableStateFlow(listOf()) /** The current list data derived from [currentGenre]. */ - val genreList: StateFlow> = _genreList - private val _genreInstructions = MutableEvent() - /** Instructions for updating [artistList] in the UI. */ - val genreInstructions: Event - get() = _genreInstructions + val genreSongList: StateFlow> = _genreSongList - /** The current [Sort] used for [Song]s in [genreList]. */ + private val _genreSongInstructions = MutableEvent() + /** Instructions for updating [artistSongList] in the UI. */ + val genreSongInstructions: Event + get() = _genreSongInstructions + + /** The current [Sort] used for [Song]s in [genreSongList]. */ var genreSongSort: Sort - get() = musicSettings.genreSongSort + get() = listSettings.genreSongSort set(value) { - musicSettings.genreSongSort = value + listSettings.genreSongSort = value // Refresh the genre list to reflect the new sort. 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 --- private val _currentPlaylist = MutableStateFlow(null) @@ -170,13 +181,14 @@ constructor( val currentPlaylist: StateFlow get() = _currentPlaylist - private val _playlistList = MutableStateFlow(listOf()) + private val _playlistSongList = MutableStateFlow(listOf()) /** The current list data derived from [currentPlaylist] */ - val playlistList: StateFlow> = _playlistList - private val _playlistInstructions = MutableEvent() - /** Instructions for updating [playlistList] in the UI. */ - val playlistInstructions: Event - get() = _playlistInstructions + val playlistSongList: StateFlow> = _playlistSongList + + private val _playlistSongInstructions = MutableEvent() + /** Instructions for updating [playlistSongList] in the UI. */ + val playlistSongInstructions: Event + get() = _playlistSongInstructions private val _editedPlaylist = MutableStateFlow?>(null) /** @@ -186,12 +198,11 @@ constructor( val editedPlaylist: StateFlow?> get() = _editedPlaylist - /** - * The [MusicMode] to use when playing a [Song] from the UI, or null to play from the currently - * shown item. - */ - val playbackMode: MusicMode? - get() = playbackSettings.inParentPlaybackMode + /** The [PlaySong] instructions to use when playing a [Song] from [Genre] details. */ + val playInPlaylistWith + get() = + playbackSettings.inParentPlaybackMode + ?: PlaySong.FromPlaylist(unlikelyToBeNull(currentPlaylist.value)) init { 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]. * * @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]. * * @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. * * @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, * [currentPlaylist] and [currentPlaylist] will be updated to align with the new album. @@ -439,6 +480,17 @@ constructor( 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. * @@ -447,7 +499,6 @@ constructor( * @return true if the song was moved, false otherwise. */ fun movePlaylistSongs(from: Int, to: Int): Boolean { - // TODO: Song re-sorting val playlist = _currentPlaylist.value ?: return false val editedPlaylist = (_editedPlaylist.value ?: return false).toMutableList() val realFrom = from - 2 @@ -531,8 +582,8 @@ constructor( } logD("Update album list to ${list.size} items with $instructions") - _albumInstructions.put(instructions) - _albumList.value = list + _albumSongInstructions.put(instructions) + _albumSongList.value = list } private fun refreshArtistList(artist: Artist, replace: Boolean = false) { @@ -594,8 +645,8 @@ constructor( } logD("Updating artist list to ${list.size} items with $instructions") - _artistInstructions.put(instructions) - _artistList.value = list.toList() + _artistSongInstructions.put(instructions) + _artistSongList.value = list.toList() } private fun refreshGenreList(genre: Genre, replace: Boolean = false) { @@ -620,8 +671,8 @@ constructor( list.addAll(genreSongSort.songs(genre.songs)) logD("Updating genre list to ${list.size} items with $instructions") - _genreInstructions.put(instructions) - _genreList.value = list + _genreSongInstructions.put(instructions) + _genreSongList.value = list } private fun refreshPlaylistList( @@ -640,8 +691,8 @@ constructor( } logD("Updating playlist list to ${list.size} items with $instructions") - _playlistInstructions.put(instructions) - _playlistList.value = list + _playlistSongInstructions.put(instructions) + _playlistSongList.value = list } /** diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 8d26371e7..8b9cf5a68 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -20,8 +20,6 @@ package org.oxycblt.auxio.detail import android.os.Bundle import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController 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.ListFragment import org.oxycblt.auxio.list.ListViewModel -import org.oxycblt.auxio.list.Menu -import org.oxycblt.auxio.list.Sort +import org.oxycblt.auxio.list.menu.Menu import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre 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.collectImmediately import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.navigateSafe +import org.oxycblt.auxio.util.overrideOnOverflowMenuClick import org.oxycblt.auxio.util.setFullWidthLookup -import org.oxycblt.auxio.util.share -import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull /** @@ -99,9 +94,12 @@ class GenreDetailFragment : // --- UI SETUP --- binding.detailNormalToolbar.apply { - inflateMenu(R.menu.toolbar_parent) setNavigationOnClickListener { findNavController().navigateUp() } setOnMenuItemClickListener(this@GenreDetailFragment) + overrideOnOverflowMenuClick { + listModel.openMenu( + R.menu.item_detail_parent, unlikelyToBeNull(detailModel.currentGenre.value)) + } } binding.detailRecycler.apply { @@ -109,7 +107,7 @@ class GenreDetailFragment : (layoutManager as GridLayoutManager).setFullWidthLookup { if (it != 0) { val item = - detailModel.genreList.value.getOrElse(it - 1) { + detailModel.genreSongList.value.getOrElse(it - 1) { return@setFullWidthLookup false } item is Divider || item is Header @@ -123,7 +121,7 @@ class GenreDetailFragment : // DetailViewModel handles most initialization from the navigation argument. detailModel.setGenre(args.genreUid) collectImmediately(detailModel.currentGenre, ::updatePlaylist) - collectImmediately(detailModel.genreList, ::updateList) + collectImmediately(detailModel.genreSongList, ::updateList) collect(detailModel.toShow.flow, ::handleShow) collect(listModel.menu.flow, ::handleMenu) collectImmediately(listModel.selected, ::updateSelection) @@ -139,63 +137,21 @@ class GenreDetailFragment : binding.detailRecycler.adapter = null // 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. - detailModel.genreInstructions.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 - } - } + detailModel.genreSongInstructions.consume() } override fun onRealClick(item: Music) { when (item) { is Artist -> detailModel.showArtist(item) - is Song -> { - 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)) - } - } + is Song -> playbackModel.play(item, detailModel.playInGenreWith) else -> error("Unexpected datatype: ${item::class.simpleName}") } } - override fun onOpenMenu(item: Music, anchor: View) { + override fun onOpenMenu(item: Music) { when (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}") } } @@ -208,31 +164,8 @@ class GenreDetailFragment : playbackModel.shuffle(unlikelyToBeNull(detailModel.currentGenre.value)) } - override fun onOpenSortMenu(anchor: View) { - openMenu(anchor, R.menu.sort_genre) { - // 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 - } - } + override fun onOpenSortMenu() { + findNavController().navigateSafe(GenreDetailFragmentDirections.sort()) } private fun updatePlaylist(genre: Genre?) { @@ -246,7 +179,7 @@ class GenreDetailFragment : } private fun updateList(list: List) { - genreListAdapter.update(list, detailModel.genreInstructions.consume()) + genreListAdapter.update(list, detailModel.genreSongInstructions.consume()) } private fun handleShow(show: Show?) { @@ -304,12 +237,10 @@ class GenreDetailFragment : if (menu == null) return val directions = when (menu) { - is Menu.ForSong -> - GenreDetailFragmentDirections.openSongMenu(menu.menuRes, menu.music.uid) - is Menu.ForArtist -> - GenreDetailFragmentDirections.openArtistMenu(menu.menuRes, menu.music.uid) + is Menu.ForSong -> GenreDetailFragmentDirections.openSongMenu(menu.parcel) + is Menu.ForArtist -> GenreDetailFragmentDirections.openArtistMenu(menu.parcel) + is Menu.ForGenre -> GenreDetailFragmentDirections.openGenreMenu(menu.parcel) is Menu.ForAlbum, - is Menu.ForGenre, is Menu.ForPlaylist -> error("Unexpected menu $menu") } findNavController().navigateSafe(directions) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt index 632352075..9a2c02777 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt @@ -20,8 +20,6 @@ package org.oxycblt.auxio.detail import android.os.Bundle import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View import androidx.fragment.app.activityViewModels import androidx.navigation.NavController 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.ListFragment 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.MusicParent 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.collectImmediately import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.navigateSafe +import org.oxycblt.auxio.util.overrideOnOverflowMenuClick import org.oxycblt.auxio.util.setFullWidthLookup -import org.oxycblt.auxio.util.share -import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.unlikelyToBeNull /** @@ -104,9 +100,13 @@ class PlaylistDetailFragment : // --- UI SETUP --- binding.detailNormalToolbar.apply { - inflateMenu(R.menu.toolbar_playlist) setNavigationOnClickListener { findNavController().navigateUp() } setOnMenuItemClickListener(this@PlaylistDetailFragment) + overrideOnOverflowMenuClick { + listModel.openMenu( + R.menu.item_detail_playlist, + unlikelyToBeNull(detailModel.currentPlaylist.value)) + } } binding.detailEditToolbar.apply { @@ -123,7 +123,7 @@ class PlaylistDetailFragment : (layoutManager as GridLayoutManager).setFullWidthLookup { if (it != 0) { val item = - detailModel.playlistList.value.getOrElse(it - 1) { + detailModel.playlistSongList.value.getOrElse(it - 1) { return@setFullWidthLookup false } item is Divider || item is Header @@ -137,7 +137,7 @@ class PlaylistDetailFragment : // DetailViewModel handles most initialization from the navigation argument. detailModel.setPlaylist(args.playlistUid) collectImmediately(detailModel.currentPlaylist, ::updatePlaylist) - collectImmediately(detailModel.playlistList, ::updateList) + collectImmediately(detailModel.playlistSongList, ::updateList) collectImmediately(detailModel.editedPlaylist, ::updateEditedList) collect(detailModel.toShow.flow, ::handleShow) collect(listModel.menu.flow, ::handleMenu) @@ -168,7 +168,7 @@ class PlaylistDetailFragment : binding.detailRecycler.adapter = null // 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. - detailModel.playlistInstructions.consume() + detailModel.playlistSongInstructions.consume() } override fun onDestinationChanged( @@ -183,61 +183,24 @@ class PlaylistDetailFragment : initialNavDestinationChange = true return } - // Drop any pending playlist edits when navigating away. This could actually happen - // if the user is quick enough. - 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 - } + 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 + // if the user is quick enough. + detailModel.dropPlaylistEdit() } } override fun onRealClick(item: Song) { - playbackModel.playFromPlaylist(item, unlikelyToBeNull(detailModel.currentPlaylist.value)) + playbackModel.play(item, detailModel.playInPlaylistWith) } override fun onPickUp(viewHolder: RecyclerView.ViewHolder) { requireNotNull(touchHelper) { "ItemTouchHelper was not available" }.startDrag(viewHolder) } - override fun onOpenMenu(item: Song, anchor: View) { - listModel.openMenu(R.menu.item_playlist_song, item) + override fun onOpenMenu(item: Song) { + listModel.openMenu(R.menu.item_playlist_song, item, detailModel.playInPlaylistWith) } override fun onPlay() { @@ -252,7 +215,9 @@ class PlaylistDetailFragment : detailModel.startPlaylistEdit() } - override fun onOpenSortMenu(anchor: View) {} + override fun onOpenSortMenu() { + findNavController().navigateSafe(PlaylistDetailFragmentDirections.sort()) + } private fun updatePlaylist(playlist: Playlist?) { if (playlist == null) { @@ -261,24 +226,14 @@ class PlaylistDetailFragment : return } val binding = requireBinding() - binding.detailNormalToolbar.apply { - 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.detailNormalToolbar.title = playlist.name.resolve(requireContext()) binding.detailEditToolbar.title = getString(R.string.fmt_editing, playlist.name.resolve(requireContext())) playlistHeaderAdapter.setParent(playlist) } private fun updateList(list: List) { - playlistListAdapter.update(list, detailModel.playlistInstructions.consume()) + playlistListAdapter.update(list, detailModel.playlistSongInstructions.consume()) } private fun updateEditedList(editedPlaylist: List?) { @@ -344,12 +299,12 @@ class PlaylistDetailFragment : if (menu == null) return val directions = when (menu) { - is Menu.ForSong -> - PlaylistDetailFragmentDirections.openSongMenu(menu.menuRes, menu.music.uid) + is Menu.ForSong -> PlaylistDetailFragmentDirections.openSongMenu(menu.parcel) + is Menu.ForPlaylist -> + PlaylistDetailFragmentDirections.openPlaylistMenu(menu.parcel) is Menu.ForArtist, is Menu.ForAlbum, - is Menu.ForGenre, - is Menu.ForPlaylist -> error("Unexpected menu $menu") + is Menu.ForGenre -> error("Unexpected menu $menu") } findNavController().navigateSafe(directions) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt index f4654ace4..efe219235 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt @@ -106,16 +106,16 @@ sealed interface ArtistShowChoices { class FromSong(val song: Song) : ArtistShowChoices { override val uid = song.uid override val choices = song.artists + override fun sanitize(newLibrary: DeviceLibrary) = newLibrary.findSong(uid)?.let { FromSong(it) } } - /** - * Backing implementation of [ArtistShowChoices] that is based on an [AlbumArtistShowChoices]. - */ + /** Backing implementation of [ArtistShowChoices] that is based on an [Album]. */ data class FromAlbum(val album: Album) : ArtistShowChoices { override val uid = album.uid override val choices = album.artists + override fun sanitize(newLibrary: DeviceLibrary) = newLibrary.findAlbum(uid)?.let { FromAlbum(it) } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt index 02303a566..cb2343219 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt @@ -41,6 +41,7 @@ class ArtistDetailHeaderAdapter(private val listener: Listener) : DetailHeaderAdapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ArtistDetailHeaderViewHolder.from(parent) + override fun onBindHeader(holder: ArtistDetailHeaderViewHolder, parent: Artist) = holder.bind(parent, listener) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt index 247875432..4afabb6c3 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt @@ -30,7 +30,9 @@ import org.oxycblt.auxio.util.logD abstract class DetailHeaderAdapter : RecyclerView.Adapter() { private var currentParent: T? = null + final override fun getItemCount() = 1 + final override fun onBindViewHolder(holder: VH, position: Int) = onBindHeader(holder, requireNotNull(currentParent)) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt index ba350e7b3..08293199f 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt @@ -82,7 +82,7 @@ abstract class DetailListAdapter( * Called when the button in a [SortHeader] item is pressed, requesting that the sort menu * should be opened. */ - fun onOpenSortMenu(anchor: View) + fun onOpenSortMenu() } 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 // button can be clear. TooltipCompat.setTooltipText(this, contentDescription) - setOnClickListener(listener::onOpenSortMenu) + setOnClickListener { listener.onOpenSortMenu() } } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt index 06c5be29b..ca8a0657b 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt @@ -170,10 +170,25 @@ private class EditHeaderViewHolder private constructor(private val binding: Item TooltipCompat.setTooltipText(this, contentDescription) setOnClickListener { listener.onStartEdit() } } + binding.headerSort.apply { + TooltipCompat.setTooltipText(this, contentDescription) + setOnClickListener { listener.onOpenSortMenu() } + } } 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 { @@ -211,6 +226,7 @@ private constructor(private val binding: ItemEditableSongBinding) : PlaylistDetailListAdapter.ViewHolder { override val enabled: Boolean get() = binding.songDragHandle.isVisible + override val root = binding.root override val body = binding.body override val delete = binding.background diff --git a/app/src/main/java/org/oxycblt/auxio/detail/sort/AlbumSongSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/sort/AlbumSongSortDialog.kt new file mode 100644 index 000000000..de8fe127d --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/sort/AlbumSongSortDialog.kt @@ -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 . + */ + +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() + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/sort/ArtistSongSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/sort/ArtistSongSortDialog.kt new file mode 100644 index 000000000..d0d4e355a --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/sort/ArtistSongSortDialog.kt @@ -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 . + */ + +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() + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/sort/GenreSongSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/sort/GenreSongSortDialog.kt new file mode 100644 index 000000000..88c69172b --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/sort/GenreSongSortDialog.kt @@ -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 . + */ + +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() + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/sort/PlaylistSongSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/sort/PlaylistSongSortDialog.kt new file mode 100644 index 000000000..923d41829 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/sort/PlaylistSongSortDialog.kt @@ -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 . + */ + +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() + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index 43dded9c6..66de36045 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -26,7 +26,6 @@ import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.MenuCompat import androidx.core.view.isVisible -import androidx.core.view.iterator import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager @@ -38,7 +37,6 @@ import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.appbar.AppBarLayout import com.google.android.material.tabs.TabLayoutMediator -import com.google.android.material.transition.MaterialFadeThrough import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint 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.Tab import org.oxycblt.auxio.list.ListViewModel -import org.oxycblt.auxio.list.Menu 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.IndexingState 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.NoAudioPermissionException 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.logW 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 @@ -102,10 +98,9 @@ class HomeFragment : // 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 // our transitions. - when (val id = savedInstanceState.getInt(KEY_LAST_TRANSITION_ID, -2)) { - -2 -> {} - -1 -> applyFadeTransition() - else -> applyAxisTransition(id) + val axis = savedInstanceState.getInt(KEY_LAST_TRANSITION_ID, -1) + if (axis > -1) { + applyAxisTransition(axis) } } } @@ -173,8 +168,8 @@ class HomeFragment : // --- VIEWMODEL SETUP --- collect(homeModel.recreateTabs.flow, ::handleRecreate) - collectImmediately(homeModel.currentTabMode, ::updateCurrentTab) - collectImmediately(homeModel.songsList, homeModel.isFastScrolling, ::updateFab) + collectImmediately(homeModel.currentTabType, ::updateCurrentTab) + collectImmediately(homeModel.songList, homeModel.isFastScrolling, ::updateFab) collect(listModel.menu.flow, ::handleMenu) collectImmediately(listModel.selected, ::updateSelection) collectImmediately(musicModel.indexingState, ::updateIndexerState) @@ -183,9 +178,9 @@ class HomeFragment : } override fun onSaveInstanceState(outState: Bundle) { - when (val transition = enterTransition) { - is MaterialFadeThrough -> outState.putInt(KEY_LAST_TRANSITION_ID, -1) - is MaterialSharedAxis -> outState.putInt(KEY_LAST_TRANSITION_ID, transition.axis) + val transition = enterTransition + if (transition is MaterialSharedAxis) { + outState.putInt(KEY_LAST_TRANSITION_ID, transition.axis) } super.onSaveInstanceState(outState) @@ -224,63 +219,42 @@ class HomeFragment : } R.id.action_settings -> { logD("Navigating to preferences") - applyFadeTransition() - findNavController().navigateSafe(HomeFragmentDirections.preferences()) + homeModel.showSettings() true } R.id.action_about -> { logD("Navigating to about") - applyFadeTransition() - findNavController().navigateSafe(HomeFragmentDirections.about()) + homeModel.showAbout() true } // Handle sort menu - R.id.submenu_sorting -> { + R.id.action_sort -> { // Junk click event when opening the menu - true - } - R.id.option_sort_asc -> { - 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)) + 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() + } + findNavController().navigateSafe(directions) true } 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") - false - } + logW("Unexpected menu item selected") + false } } } private fun setupPager(binding: FragmentHomeBinding) { binding.homePager.adapter = - HomePagerAdapter(homeModel.currentTabModes, childFragmentManager, viewLifecycleOwner) + HomePagerAdapter(homeModel.currentTabTypes, childFragmentManager, viewLifecycleOwner) 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 // behavior. logD("Single tab shown, disabling TabLayout") @@ -298,81 +272,26 @@ class HomeFragment : TabLayoutMediator( binding.homeTabs, binding.homePager, - AdaptiveTabStrategy(requireContext(), homeModel.currentTabModes)) + AdaptiveTabStrategy(requireContext(), homeModel.currentTabTypes)) .attach() } - private fun updateCurrentTab(tabMode: MusicMode) { + private fun updateCurrentTab(tabType: MusicType) { 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 // scrolling state. This prevents the lift state from being confused as one // goes between different tabs. binding.homeAppbar.liftOnScrollTargetViewId = - when (tabMode) { - MusicMode.SONGS -> R.id.home_song_recycler - MusicMode.ALBUMS -> R.id.home_album_recycler - MusicMode.ARTISTS -> R.id.home_artist_recycler - MusicMode.GENRES -> R.id.home_genre_recycler - MusicMode.PLAYLISTS -> R.id.home_playlist_recycler + when (tabType) { + MusicType.SONGS -> R.id.home_song_recycler + MusicType.ALBUMS -> R.id.home_album_recycler + MusicType.ARTISTS -> R.id.home_artist_recycler + MusicType.GENRES -> R.id.home_genre_recycler + MusicType.PLAYLISTS -> R.id.home_playlist_recycler } - if (tabMode != MusicMode.PLAYLISTS) { + if (tabType != MusicType.PLAYLISTS) { logD("Flipping to shuffle button") binding.homeFab.flipTo(R.drawable.ic_shuffle_off_24, R.string.desc_shuffle_all) { playbackModel.shuffleAll() @@ -577,15 +496,11 @@ class HomeFragment : if (menu == null) return val directions = when (menu) { - is Menu.ForSong -> HomeFragmentDirections.openSongMenu(menu.menuRes, menu.music.uid) - is Menu.ForAlbum -> - HomeFragmentDirections.openAlbumMenu(menu.menuRes, menu.music.uid) - is Menu.ForArtist -> - HomeFragmentDirections.openArtistMenu(menu.menuRes, menu.music.uid) - is Menu.ForGenre -> - HomeFragmentDirections.openGenreMenu(menu.menuRes, menu.music.uid) - is Menu.ForPlaylist -> - HomeFragmentDirections.openPlaylistMenu(menu.menuRes, menu.music.uid) + is Menu.ForSong -> HomeFragmentDirections.openSongMenu(menu.parcel) + is Menu.ForAlbum -> HomeFragmentDirections.openAlbumMenu(menu.parcel) + is Menu.ForArtist -> HomeFragmentDirections.openArtistMenu(menu.parcel) + is Menu.ForGenre -> HomeFragmentDirections.openGenreMenu(menu.parcel) + is Menu.ForPlaylist -> HomeFragmentDirections.openPlaylistMenu(menu.parcel) } findNavController().navigateSafe(directions) } @@ -616,13 +531,6 @@ class HomeFragment : reenterTransition = MaterialSharedAxis(axis, false) } - private fun applyFadeTransition() { - enterTransition = MaterialFadeThrough() - returnTransition = MaterialFadeThrough() - exitTransition = MaterialFadeThrough() - reenterTransition = MaterialFadeThrough() - } - /** * [FragmentStateAdapter] implementation for the [HomeFragment]'s [ViewPager2] instance. * @@ -632,18 +540,19 @@ class HomeFragment : * [FragmentStateAdapter]. */ private class HomePagerAdapter( - private val tabs: List, + private val tabs: List, fragmentManager: FragmentManager, lifecycleOwner: LifecycleOwner ) : FragmentStateAdapter(fragmentManager, lifecycleOwner.lifecycle) { override fun getItemCount() = tabs.size + override fun createFragment(position: Int): Fragment = when (tabs[position]) { - MusicMode.SONGS -> SongListFragment() - MusicMode.ALBUMS -> AlbumListFragment() - MusicMode.ARTISTS -> ArtistListFragment() - MusicMode.GENRES -> GenreListFragment() - MusicMode.PLAYLISTS -> PlaylistListFragment() + MusicType.SONGS -> SongListFragment() + MusicType.ALBUMS -> AlbumListFragment() + MusicType.ARTISTS -> ArtistListFragment() + MusicType.GENRES -> GenreListFragment() + MusicType.PLAYLISTS -> PlaylistListFragment() } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt index 4e468ec95..5fc218cfe 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt @@ -24,7 +24,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.oxycblt.auxio.R 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.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull @@ -75,9 +75,9 @@ class HomeSettingsImpl @Inject constructor(@ApplicationContext context: Context) logD("Old tabs: $oldTabs") // 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 - oldTabs[playlistIndex] = Tab.Visible(MusicMode.PLAYLISTS) + oldTabs[playlistIndex] = Tab.Visible(MusicType.PLAYLISTS) logD("New tabs: $oldTabs") sharedPreferences.edit { diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index 4e471758a..bb9311c84 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -24,16 +24,17 @@ import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow 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.sort.Sort import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre -import org.oxycblt.auxio.music.MusicMode 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.Song +import org.oxycblt.auxio.playback.PlaySong import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent @@ -49,73 +50,98 @@ class HomeViewModel @Inject constructor( private val homeSettings: HomeSettings, + private val listSettings: ListSettings, private val playbackSettings: PlaybackSettings, private val musicRepository: MusicRepository, - private val musicSettings: MusicSettings ) : ViewModel(), MusicRepository.UpdateListener, HomeSettings.Listener { - private val _songsList = MutableStateFlow(listOf()) + private val _songList = MutableStateFlow(listOf()) /** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */ - val songsList: StateFlow> - get() = _songsList - private val _songsInstructions = MutableEvent() - /** Instructions for how to update [songsList] in the UI. */ - val songsInstructions: Event - get() = _songsInstructions + val songList: StateFlow> + get() = _songList - private val _albumsLists = MutableStateFlow(listOf()) + private val _songInstructions = MutableEvent() + /** Instructions for how to update [songList] in the UI. */ + val songInstructions: Event + 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()) /** A list of [Album]s, sorted by the preferred [Sort], to be shown in the home view. */ - val albumsList: StateFlow> - get() = _albumsLists - private val _albumsInstructions = MutableEvent() - /** Instructions for how to update [albumsList] in the UI. */ - val albumsInstructions: Event - get() = _albumsInstructions + val albumList: StateFlow> + get() = _albumList - private val _artistsList = MutableStateFlow(listOf()) + private val _albumInstructions = MutableEvent() + /** Instructions for how to update [albumList] in the UI. */ + val albumInstructions: Event + get() = _albumInstructions + + /** The current [Sort] used for [albumList]. */ + val albumSort: Sort + get() = listSettings.albumSort + + private val _artistList = MutableStateFlow(listOf()) /** * 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. */ - val artistsList: MutableStateFlow> - get() = _artistsList - private val _artistsInstructions = MutableEvent() - /** Instructions for how to update [artistsList] in the UI. */ - val artistsInstructions: Event - get() = _artistsInstructions + val artistList: MutableStateFlow> + get() = _artistList - private val _genresList = MutableStateFlow(listOf()) + private val _artistInstructions = MutableEvent() + /** Instructions for how to update [artistList] in the UI. */ + val artistInstructions: Event + get() = _artistInstructions + + /** The current [Sort] used for [artistList]. */ + val artistSort: Sort + get() = listSettings.artistSort + + private val _genreList = MutableStateFlow(listOf()) /** A list of [Genre]s, sorted by the preferred [Sort], to be shown in the home view. */ - val genresList: StateFlow> - get() = _genresList - private val _genresInstructions = MutableEvent() - /** Instructions for how to update [genresList] in the UI. */ - val genresInstructions: Event - get() = _genresInstructions + val genreList: StateFlow> + get() = _genreList - private val _playlistsList = MutableStateFlow(listOf()) + private val _genreInstructions = MutableEvent() + /** Instructions for how to update [genreList] in the UI. */ + val genreInstructions: Event + get() = _genreInstructions + + /** The current [Sort] used for [genreList]. */ + val genreSort: Sort + get() = listSettings.genreSort + + private val _playlistList = MutableStateFlow(listOf()) /** A list of [Playlist]s, sorted by the preferred [Sort], to be shown in the home view. */ - val playlistsList: StateFlow> - get() = _playlistsList - private val _playlistsInstructions = MutableEvent() - /** Instructions for how to update [genresList] in the UI. */ - val playlistsInstructions: Event - get() = _playlistsInstructions + val playlistList: StateFlow> + get() = _playlistList - /** The [MusicMode] to use when playing a [Song] from the UI. */ - val playbackMode: MusicMode - get() = playbackSettings.inListPlaybackMode + private val _playlistInstructions = MutableEvent() + /** Instructions for how to update [genreList] in the UI. */ + val playlistInstructions: Event + 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. */ - var currentTabModes = makeTabModes() + var currentTabTypes = makeTabTypes() private set - private val _currentTabMode = MutableStateFlow(currentTabModes[0]) - /** The [MusicMode] of the currently shown [Tab]. */ - val currentTabMode: StateFlow = _currentTabMode + private val _currentTabType = MutableStateFlow(currentTabTypes[0]) + /** The [MusicType] of the currently shown [Tab]. */ + val currentTabType: StateFlow = _currentTabType private val _shouldRecreate = MutableEvent() /** @@ -130,6 +156,10 @@ constructor( /** A marker for whether the user is fast-scrolling in the home view or not. */ val isFastScrolling: StateFlow = _isFastScrolling + private val _showOuter = MutableEvent() + val showOuter: Event + get() = _showOuter + init { musicRepository.addUpdateListener(this) homeSettings.registerListener(this) @@ -147,13 +177,13 @@ constructor( logD("Refreshing library") // Get the each list of items in the library to use as our list data. // Applying the preferred sorting to them. - _songsInstructions.put(UpdateInstructions.Diff) - _songsList.value = musicSettings.songSort.songs(deviceLibrary.songs) - _albumsInstructions.put(UpdateInstructions.Diff) - _albumsLists.value = musicSettings.albumSort.albums(deviceLibrary.albums) - _artistsInstructions.put(UpdateInstructions.Diff) - _artistsList.value = - musicSettings.artistSort.artists( + _songInstructions.put(UpdateInstructions.Diff) + _songList.value = listSettings.songSort.songs(deviceLibrary.songs) + _albumInstructions.put(UpdateInstructions.Diff) + _albumList.value = listSettings.albumSort.albums(deviceLibrary.albums) + _artistInstructions.put(UpdateInstructions.Diff) + _artistList.value = + listSettings.artistSort.artists( if (homeSettings.shouldHideCollaborators) { logD("Filtering collaborator artists") // Hide Collaborators is enabled, filter out collaborators. @@ -162,22 +192,22 @@ constructor( logD("Using all artists") deviceLibrary.artists }) - _genresInstructions.put(UpdateInstructions.Diff) - _genresList.value = musicSettings.genreSort.genres(deviceLibrary.genres) + _genreInstructions.put(UpdateInstructions.Diff) + _genreList.value = listSettings.genreSort.genres(deviceLibrary.genres) } val userLibrary = musicRepository.userLibrary if (changes.userLibrary && userLibrary != null) { logD("Refreshing playlists") - _playlistsInstructions.put(UpdateInstructions.Diff) - _playlistsList.value = musicSettings.playlistSort.playlists(userLibrary.playlists) + _playlistInstructions.put(UpdateInstructions.Diff) + _playlistList.value = listSettings.playlistSort.playlists(userLibrary.playlists) } } override fun onTabsChanged() { // Tabs changed, update the current tabs and set up a re-create event. - currentTabModes = makeTabModes() - logD("Updating tabs: ${currentTabMode.value}") + currentTabTypes = makeTabTypes() + logD("Updating tabs: ${currentTabType.value}") _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. - * @return The [Sort] preferred for that [Tab] + * @param sort The [Sort] to apply. */ - fun getSortForTab(tabMode: MusicMode) = - when (tabMode) { - MusicMode.SONGS -> musicSettings.songSort - MusicMode.ALBUMS -> musicSettings.albumSort - MusicMode.ARTISTS -> musicSettings.artistSort - MusicMode.GENRES -> musicSettings.genreSort - MusicMode.PLAYLISTS -> musicSettings.playlistSort - } - - /** - * Update the preferred [Sort] for the current [Tab]. Will update corresponding list. - * - * @param sort The new [Sort] to apply. Assumed to be an allowed sort for the current [Tab]. - */ - fun setSortForCurrentTab(sort: Sort) { - // Can simply re-sort the current list of items without having to access the library. - when (val mode = _currentTabMode.value) { - MusicMode.SONGS -> { - 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) - } - } + fun applySongSort(sort: Sort) { + listSettings.songSort = sort + _songInstructions.put(UpdateInstructions.Replace(0)) + _songList.value = listSettings.songSort.songs(_songList.value) } /** - * Update [currentTabMode] to reflect a new ViewPager2 position + * Apply a new [Sort] to [albumList]. + * + * @param sort The [Sort] to apply. + */ + fun applyAlbumSort(sort: Sort) { + listSettings.albumSort = sort + _albumInstructions.put(UpdateInstructions.Replace(0)) + _albumList.value = listSettings.albumSort.albums(_albumList.value) + } + + /** + * 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. */ fun synchronizeTabPosition(pagerPos: Int) { - logD("Updating current tab to ${currentTabModes[pagerPos]}") - _currentTabMode.value = currentTabModes[pagerPos] + logD("Updating current tab to ${currentTabTypes[pagerPos]}") + _currentTabType.value = currentTabTypes[pagerPos] } /** @@ -264,12 +293,26 @@ constructor( _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. */ - private fun makeTabModes() = - homeSettings.homeTabs.filterIsInstance().map { it.mode } + private fun makeTabTypes() = + homeSettings.homeTabs.filterIsInstance().map { it.type } +} + +sealed interface Outer { + data object Settings : Outer + + data object About : Outer } diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt index 620ac018f..ae546d137 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt @@ -123,8 +123,11 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) } override fun isAutoMirrored(): Boolean = true + override fun setAlpha(alpha: Int) {} + override fun setColorFilter(colorFilter: ColorFilter?) {} + override fun getOpacity(): Int = PixelFormat.TRANSLUCENT private fun updatePath() { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index 984551733..a7c63a455 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -21,7 +21,6 @@ package org.oxycblt.auxio.home.list import android.os.Bundle import android.text.format.DateUtils import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.fragment.app.activityViewModels 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.ListViewModel import org.oxycblt.auxio.list.SelectableListListener -import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter 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.Music -import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song @@ -81,7 +79,7 @@ class AlbumListFragment : listener = this@AlbumListFragment } - collectImmediately(homeModel.albumsList, ::updateAlbums) + collectImmediately(homeModel.albumList, ::updateAlbums) collectImmediately(listModel.selected, ::updateSelection) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) @@ -97,9 +95,9 @@ class AlbumListFragment : } 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. - return when (homeModel.getSortForTab(MusicMode.ALBUMS).mode) { + return when (homeModel.albumSort.mode) { // By Name -> Use Name is Sort.Mode.ByName -> album.name.thumb @@ -141,12 +139,12 @@ class AlbumListFragment : detailModel.showAlbum(item) } - override fun onOpenMenu(item: Album, anchor: View) { + override fun onOpenMenu(item: Album) { listModel.openMenu(R.menu.item_album, item) } private fun updateAlbums(albums: List) { - albumAdapter.update(albums, homeModel.albumsInstructions.consume()) + albumAdapter.update(albums, homeModel.albumInstructions.consume()) } private fun updateSelection(selection: List) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt index ab9928003..84834cb74 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt @@ -20,7 +20,6 @@ package org.oxycblt.auxio.home.list import android.os.Bundle import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.fragment.app.activityViewModels 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.ListViewModel import org.oxycblt.auxio.list.SelectableListListener -import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter 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.Music -import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song @@ -76,7 +74,7 @@ class ArtistListFragment : listener = this@ArtistListFragment } - collectImmediately(homeModel.artistsList, ::updateArtists) + collectImmediately(homeModel.artistList, ::updateArtists) collectImmediately(listModel.selected, ::updateSelection) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) @@ -92,9 +90,9 @@ class ArtistListFragment : } 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. - return when (homeModel.getSortForTab(MusicMode.ARTISTS).mode) { + return when (homeModel.artistSort.mode) { // By Name -> Use Name is Sort.Mode.ByName -> artist.name.thumb @@ -117,12 +115,12 @@ class ArtistListFragment : detailModel.showArtist(item) } - override fun onOpenMenu(item: Artist, anchor: View) { + override fun onOpenMenu(item: Artist) { listModel.openMenu(R.menu.item_parent, item) } private fun updateArtists(artists: List) { - artistAdapter.update(artists, homeModel.artistsInstructions.consume()) + artistAdapter.update(artists, homeModel.artistInstructions.consume()) } private fun updateSelection(selection: List) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt index 2ecd73f90..a39c0ee2d 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt @@ -20,7 +20,6 @@ package org.oxycblt.auxio.home.list import android.os.Bundle import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.fragment.app.activityViewModels 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.ListViewModel import org.oxycblt.auxio.list.SelectableListListener -import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter 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.Music -import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song @@ -75,7 +73,7 @@ class GenreListFragment : listener = this@GenreListFragment } - collectImmediately(homeModel.genresList, ::updateGenres) + collectImmediately(homeModel.genreList, ::updateGenres) collectImmediately(listModel.selected, ::updateSelection) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) @@ -91,9 +89,9 @@ class GenreListFragment : } 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. - return when (homeModel.getSortForTab(MusicMode.GENRES).mode) { + return when (homeModel.genreSort.mode) { // By Name -> Use Name is Sort.Mode.ByName -> genre.name.thumb @@ -116,12 +114,12 @@ class GenreListFragment : detailModel.showGenre(item) } - override fun onOpenMenu(item: Genre, anchor: View) { + override fun onOpenMenu(item: Genre) { listModel.openMenu(R.menu.item_parent, item) } private fun updateGenres(genres: List) { - genreAdapter.update(genres, homeModel.genresInstructions.consume()) + genreAdapter.update(genres, homeModel.genreInstructions.consume()) } private fun updateSelection(selection: List) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt index 299ceb39c..d8a7ac175 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt @@ -20,7 +20,6 @@ package org.oxycblt.auxio.home.list import android.os.Bundle import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.fragment.app.activityViewModels 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.ListViewModel import org.oxycblt.auxio.list.SelectableListListener -import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter 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.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Playlist @@ -73,7 +71,7 @@ class PlaylistListFragment : listener = this@PlaylistListFragment } - collectImmediately(homeModel.playlistsList, ::updatePlaylists) + collectImmediately(homeModel.playlistList, ::updatePlaylists) collectImmediately(listModel.selected, ::updateSelection) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) @@ -89,9 +87,9 @@ class PlaylistListFragment : } 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. - return when (homeModel.getSortForTab(MusicMode.GENRES).mode) { + return when (homeModel.playlistSort.mode) { // By Name -> Use Name is Sort.Mode.ByName -> playlist.name.thumb @@ -114,12 +112,12 @@ class PlaylistListFragment : detailModel.showPlaylist(item) } - override fun onOpenMenu(item: Playlist, anchor: View) { + override fun onOpenMenu(item: Playlist) { listModel.openMenu(R.menu.item_playlist, item) } private fun updatePlaylists(playlists: List) { - playlistAdapter.update(playlists, homeModel.playlistsInstructions.consume()) + playlistAdapter.update(playlists, homeModel.playlistInstructions.consume()) } private fun updateSelection(selection: List) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index aba66c5e0..fb214b76b 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -21,7 +21,6 @@ package org.oxycblt.auxio.home.list import android.os.Bundle import android.text.format.DateUtils import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.fragment.app.activityViewModels 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.ListViewModel import org.oxycblt.auxio.list.SelectableListListener -import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter 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.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song @@ -78,7 +76,7 @@ class SongListFragment : listener = this@SongListFragment } - collectImmediately(homeModel.songsList, ::updateSongs) + collectImmediately(homeModel.songList, ::updateSongs) collectImmediately(listModel.selected, ::updateSelection) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) @@ -94,11 +92,11 @@ class SongListFragment : } 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. // 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. - return when (homeModel.getSortForTab(MusicMode.SONGS).mode) { + return when (homeModel.songSort.mode) { // Name -> Use name is Sort.Mode.ByName -> song.name.thumb @@ -137,15 +135,15 @@ class SongListFragment : } override fun onRealClick(item: Song) { - playbackModel.playFrom(item, homeModel.playbackMode) + playbackModel.play(item, homeModel.playWith) } - override fun onOpenMenu(item: Song, anchor: View) { - listModel.openMenu(R.menu.item_song, item) + override fun onOpenMenu(item: Song) { + listModel.openMenu(R.menu.item_song, item, homeModel.playWith) } private fun updateSongs(songs: List) { - songAdapter.update(songs, homeModel.songsInstructions.consume()) + songAdapter.update(songs, homeModel.songInstructions.consume()) } private fun updateSelection(selection: List) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/sort/AlbumSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/sort/AlbumSortDialog.kt new file mode 100644 index 000000000..39efb1ca1 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/home/sort/AlbumSortDialog.kt @@ -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 . + */ + +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) +} diff --git a/app/src/main/java/org/oxycblt/auxio/home/sort/ArtistSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/sort/ArtistSortDialog.kt new file mode 100644 index 000000000..f3aeacd17 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/home/sort/ArtistSortDialog.kt @@ -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 . + */ + +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) +} diff --git a/app/src/main/java/org/oxycblt/auxio/home/sort/GenreSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/sort/GenreSortDialog.kt new file mode 100644 index 000000000..e62ac8272 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/home/sort/GenreSortDialog.kt @@ -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 . + */ + +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) +} diff --git a/app/src/main/java/org/oxycblt/auxio/home/sort/PlaylistSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/sort/PlaylistSortDialog.kt new file mode 100644 index 000000000..d87f126e9 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/home/sort/PlaylistSortDialog.kt @@ -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 . + */ + +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) +} diff --git a/app/src/main/java/org/oxycblt/auxio/home/sort/SongSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/sort/SongSortDialog.kt new file mode 100644 index 000000000..a961a39c4 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/home/sort/SongSortDialog.kt @@ -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 . + */ + +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) +} diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt index 36aed93bf..73170ef4c 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt @@ -22,7 +22,7 @@ import android.content.Context import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator 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 @@ -32,7 +32,7 @@ import org.oxycblt.auxio.music.MusicMode * @param tabs Current tab configuration from settings * @author Alexander Capehart (OxygenCobalt) */ -class AdaptiveTabStrategy(context: Context, private val tabs: List) : +class AdaptiveTabStrategy(context: Context, private val tabs: List) : TabLayoutMediator.TabConfigurationStrategy { private val width = context.resources.configuration.smallestScreenWidthDp @@ -41,23 +41,23 @@ class AdaptiveTabStrategy(context: Context, private val tabs: List) : val string: Int when (tabs[position]) { - MusicMode.SONGS -> { + MusicType.SONGS -> { icon = R.drawable.ic_song_24 string = R.string.lbl_songs } - MusicMode.ALBUMS -> { + MusicType.ALBUMS -> { icon = R.drawable.ic_album_24 string = R.string.lbl_albums } - MusicMode.ARTISTS -> { + MusicType.ARTISTS -> { icon = R.drawable.ic_artist_24 string = R.string.lbl_artists } - MusicMode.GENRES -> { + MusicType.GENRES -> { icon = R.drawable.ic_genre_24 string = R.string.lbl_genres } - MusicMode.PLAYLISTS -> { + MusicType.PLAYLISTS -> { icon = R.drawable.ic_playlist_24 string = R.string.lbl_playlists } diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt index 5cacd084b..aee964e45 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt @@ -18,30 +18,30 @@ 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.logW /** * 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) */ -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. * - * @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. * - * @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 { // 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 - /** 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 = arrayOf( - MusicMode.SONGS, - MusicMode.ALBUMS, - MusicMode.ARTISTS, - MusicMode.GENRES, - MusicMode.PLAYLISTS) + MusicType.SONGS, + MusicType.ALBUMS, + MusicType.ARTISTS, + MusicType.GENRES, + MusicType.PLAYLISTS) /** * 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): Int { // 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) { logW( "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) { val bin = when (tab) { - is Visible -> 1.shl(3) or MODE_TABLE.indexOf(tab.mode) - is Invisible -> MODE_TABLE.indexOf(tab.mode) + is Visible -> 1.shl(3) or MODE_TABLE.indexOf(tab.type) + is Invisible -> MODE_TABLE.indexOf(tab.type) } sequence = sequence or bin.shl(shift) @@ -131,7 +131,7 @@ sealed class Tab(open val mode: MusicMode) { } // Make sure there are no duplicate tabs - val distinct = tabs.distinctBy { it.mode } + val distinct = tabs.distinctBy { it.type } if (tabs.size != distinct.size) { logW( "Tab sequences should not have duplicates [old: ${tabs.size} new: ${distinct.size}]") diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt index 277c0c39b..736b5ba30 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt @@ -26,7 +26,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemTabBinding import org.oxycblt.auxio.list.EditClickListListener 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.logD @@ -42,7 +42,9 @@ class TabAdapter(private val listener: EditClickListListener) : private set override fun getItemCount() = tabs.size + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = TabViewHolder.from(parent) + override fun onBindViewHolder(holder: TabViewHolder, position: Int) { holder.bind(tabs[position], listener) } @@ -107,14 +109,14 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) : fun bind(tab: Tab, listener: EditClickListListener) { listener.bind(tab, this, dragHandle = binding.tabDragHandle) binding.tabCheckBox.apply { - // Update the CheckBox name to align with the mode + // Update the CheckBox name to align with the type setText( - when (tab.mode) { - MusicMode.SONGS -> R.string.lbl_songs - MusicMode.ALBUMS -> R.string.lbl_albums - MusicMode.ARTISTS -> R.string.lbl_artists - MusicMode.GENRES -> R.string.lbl_genres - MusicMode.PLAYLISTS -> R.string.lbl_playlists + when (tab.type) { + MusicType.SONGS -> R.string.lbl_songs + MusicType.ALBUMS -> R.string.lbl_albums + MusicType.ARTISTS -> R.string.lbl_artists + MusicType.GENRES -> R.string.lbl_genres + MusicType.PLAYLISTS -> R.string.lbl_playlists }) // Unlike in other adapters, we update the checked state alongside diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt index fd6b36ae7..57bc73c15 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt @@ -91,13 +91,13 @@ class TabCustomizeDialog : override fun onClick(item: Tab, viewHolder: RecyclerView.ViewHolder) { // We will need the exact index of the tab to update on in order to // 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 new = when (old) { // Invert the visibility of the tab - is Tab.Visible -> Tab.Invisible(old.mode) - is Tab.Invisible -> Tab.Visible(old.mode) + is Tab.Visible -> Tab.Invisible(old.type) + is Tab.Invisible -> Tab.Visible(old.type) } logD("Flipping tab visibility [from: $old to: $new]") tabAdapter.setTab(index, new) diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt index 41662f93a..f2858479c 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt @@ -76,7 +76,7 @@ import org.oxycblt.auxio.util.getInteger class CoverView @JvmOverloads 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 uiSettings: UISettings @Inject lateinit var imageSettings: ImageSettings @@ -88,6 +88,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr val playingDrawable: AnimationDrawable, val pausedDrawable: Drawable ) + private val playbackIndicator: PlaybackIndicator? private val selectionBadge: ImageView? @@ -105,6 +106,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr val desc: String, @DrawableRes val errorRes: Int ) + private var currentCover: Cover? = null init { @@ -152,9 +154,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } else { null } - - imageSettings.registerListener(this) - uiSettings.registerListener(this) } 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) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) @@ -274,7 +255,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } private fun getCornerRadiusRes() = - if (uiSettings.roundMode) { + if (!isInEditMode && uiSettings.roundMode) { SIZING_CORNER_RADII[sizing] } else { null diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt index 537d8e874..899867eb0 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt @@ -50,7 +50,7 @@ import okio.buffer import okio.source import org.oxycblt.auxio.image.CoverMode 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.Song import org.oxycblt.auxio.util.logE diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt index 4d2d45c14..546b03a49 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt @@ -18,14 +18,9 @@ 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.viewbinding.ViewBinding import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.util.logD /** * A Fragment containing a selectable list. @@ -34,14 +29,6 @@ import org.oxycblt.auxio.util.logD */ abstract class ListFragment : SelectionFragment(), SelectableListListener { - 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 * less corresponds to an [onClick] implementation in a non-[ListFragment]. @@ -63,30 +50,4 @@ abstract class ListFragment : final override fun onSelect(item: T) { 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() - } - } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListModule.kt b/app/src/main/java/org/oxycblt/auxio/list/ListModule.kt new file mode 100644 index 000000000..521bc4283 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/list/ListModule.kt @@ -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 . + */ + +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 +} diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListSettings.kt b/app/src/main/java/org/oxycblt/auxio/list/ListSettings.kt new file mode 100644 index 000000000..3f3388b73 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/list/ListSettings.kt @@ -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 . + */ + +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 { + /** 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(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() + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt index 217865df7..ed6536fe2 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt @@ -24,15 +24,16 @@ import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import org.oxycblt.auxio.list.menu.Menu 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.MusicParent import org.oxycblt.auxio.music.MusicRepository -import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.playback.PlaySong import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent import org.oxycblt.auxio.util.logD @@ -46,10 +47,8 @@ import org.oxycblt.auxio.util.logW @HiltViewModel class ListViewModel @Inject -constructor( - private val musicRepository: MusicRepository, - private val musicSettings: MusicSettings -) : ViewModel(), MusicRepository.UpdateListener { +constructor(private val listSettings: ListSettings, private val musicRepository: MusicRepository) : + ViewModel(), MusicRepository.UpdateListener { private val _selected = MutableStateFlow(listOf()) /** The currently selected items. These are ordered in earliest selected and latest selected. */ val selected: StateFlow> @@ -121,9 +120,9 @@ constructor( .flatMap { when (it) { is Song -> listOf(it) - is Album -> musicSettings.albumSongSort.songs(it.songs) - is Artist -> musicSettings.artistSongSort.songs(it.songs) - is Genre -> musicSettings.genreSongSort.songs(it.songs) + is Album -> listSettings.albumSongSort.songs(it.songs) + is Artist -> listSettings.artistSongSort.songs(it.songs) + is Genre -> listSettings.genreSongSort.songs(it.songs) is Playlist -> it.songs } } @@ -146,10 +145,12 @@ constructor( * * @param menuRes The resource of the menu to use. * @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") - openImpl(Menu.ForSong(menuRes, song)) + openImpl(Menu.ForSong(menuRes, song, playWith)) } /** @@ -209,26 +210,3 @@ constructor( _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 -} diff --git a/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt b/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt index d728a6142..c7704e503 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt @@ -115,9 +115,8 @@ interface SelectableListListener : ClickableListListener { * 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 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. @@ -148,6 +147,6 @@ interface SelectableListListener : ClickableListListener { true } // Map the menu button to the menu opening listener. - menuButton.setOnClickListener { onOpenMenu(item, it) } + menuButton.setOnClickListener { onOpenMenu(item) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt index 977c367c4..f01b47f41 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt @@ -37,6 +37,7 @@ abstract class FlexibleListAdapter( diffCallback: DiffUtil.ItemCallback ) : RecyclerView.Adapter() { @Suppress("LeakingThis") private val differ = FlexibleListDiffer(this, diffCallback) + final override fun getItemCount() = differ.currentList.size /** The current list stored by the adapter's differ instance. */ val currentList: List @@ -69,7 +70,7 @@ abstract class FlexibleListAdapter( */ sealed interface UpdateInstructions { /** 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]. @@ -118,6 +119,7 @@ private class FlexibleListDiffer( private class MainThreadExecutor : Executor { val mHandler = Handler(Looper.getMainLooper()) + override fun execute(command: Runnable) { mHandler.post(command) } diff --git a/app/src/main/java/org/oxycblt/auxio/list/menu/Menu.kt b/app/src/main/java/org/oxycblt/auxio/list/menu/Menu.kt new file mode 100644 index 000000000..fc388cd36 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/list/menu/Menu.kt @@ -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 . + */ + +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 + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragment.kt index 305234449..907cef049 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragment.kt @@ -31,7 +31,6 @@ import org.oxycblt.auxio.databinding.DialogMenuBinding import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.list.adapter.UpdateInstructions -import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.ui.ViewBindingBottomSheetDialogFragment import org.oxycblt.auxio.util.collectImmediately 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 */ -abstract class MenuDialogFragment : +abstract class MenuDialogFragment : ViewBindingBottomSheetDialogFragment(), ClickableListListener { protected abstract val menuModel: MenuViewModel protected abstract val listModel: ListViewModel private val menuAdapter = MenuItemAdapter(@Suppress("LeakingThis") this) - /** The android resource ID of the menu options to display in the dialog. */ - abstract val menuRes: Int - - /** The [Music.UID] of the [T] to display menu options for. */ - abstract val uid: Music.UID + abstract val parcel: Menu.Parcel /** - * 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 + abstract fun getDisabledItemIds(menu: M): Set /** - * 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 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. * * @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) @@ -94,8 +89,8 @@ abstract class MenuDialogFragment : // --- VIEWMODEL SETUP --- listModel.menu.consume() - menuModel.setMusic(uid) - collectImmediately(menuModel.currentMusic, this::updateMusic) + menuModel.setMenu(parcel) + collectImmediately(menuModel.currentMenu, this::updateMenu) } override fun onDestroyBinding(binding: DialogMenuBinding) { @@ -105,23 +100,25 @@ abstract class MenuDialogFragment : binding.menuOptionRecycler.adapter = null } - private fun updateMusic(music: Music?) { - if (music == null) { - logD("No music to show, navigating away") + private fun updateMenu(menu: Menu?) { + if (menu == null) { + logD("No menu to show, navigating away") 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). // 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. @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 - val disabledIds = getDisabledItemIds(castedMusic) + val disabledIds = getDisabledItemIds(casted) val visible = builder.children.mapTo(mutableListOf()) { it.isEnabled = !disabledIds.contains(it.itemId) @@ -130,7 +127,7 @@ abstract class MenuDialogFragment : menuAdapter.update(visible, UpdateInstructions.Diff) // Delegate to impl how to show music - updateMusic(requireBinding(), castedMusic) + updateMenu(requireBinding(), casted) } final override fun onClick(item: MenuItem, viewHolder: RecyclerView.ViewHolder) { @@ -138,6 +135,6 @@ abstract class MenuDialogFragment : // TODO: This should change if the app is 100% migrated to menu dialogs findNavController().navigateUp() // 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) } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragmentImpl.kt b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragmentImpl.kt index 4aad05948..9abf34133 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragmentImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuDialogFragmentImpl.kt @@ -27,10 +27,8 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogMenuBinding import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.list.ListViewModel -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.MusicViewModel import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song @@ -46,7 +44,7 @@ import org.oxycblt.auxio.util.showToast * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint -class SongMenuDialogFragment : MenuDialogFragment() { +class SongMenuDialogFragment : MenuDialogFragment() { override val menuModel: MenuViewModel by activityViewModels() override val listModel: ListViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels() @@ -54,38 +52,37 @@ class SongMenuDialogFragment : MenuDialogFragment() { private val playbackModel: PlaybackViewModel by activityViewModels() private val args: SongMenuDialogFragmentArgs by navArgs() - override val menuRes: Int - get() = args.menuRes - override val uid: Music.UID - get() = args.songUid + override val parcel + get() = args.parcel // Nothing to disable in song menus. - override fun getDisabledItemIds(music: Song) = setOf() + override fun getDisabledItemIds(menu: Menu.ForSong) = setOf() - override fun updateMusic(binding: DialogMenuBinding, music: Song) { + override fun updateMenu(binding: DialogMenuBinding, menu: Menu.ForSong) { val context = requireContext() - binding.menuCover.bind(music) + binding.menuCover.bind(menu.song) binding.menuType.text = getString(R.string.lbl_song) - binding.menuName.text = music.name.resolve(context) - binding.menuInfo.text = music.artists.resolveNames(context) + binding.menuName.text = menu.song.name.resolve(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) { - // 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 -> { - playbackModel.playNext(music) + playbackModel.playNext(menu.song) requireContext().showToast(R.string.lng_queue_added) } R.id.action_queue_add -> { - playbackModel.addToQueue(music) + playbackModel.addToQueue(menu.song) requireContext().showToast(R.string.lng_queue_added) } - R.id.action_artist_details -> detailModel.showArtist(music) - R.id.action_album_details -> detailModel.showAlbum(music) - R.id.action_share -> requireContext().share(music) - R.id.action_playlist_add -> musicModel.addToPlaylist(music) - R.id.action_detail -> detailModel.showSong(music) + R.id.action_artist_details -> detailModel.showArtist(menu.song) + R.id.action_album_details -> detailModel.showAlbum(menu.song.album) + R.id.action_share -> requireContext().share(menu.song) + R.id.action_playlist_add -> musicModel.addToPlaylist(menu.song) + R.id.action_detail -> detailModel.showSong(menu.song) else -> error("Unexpected menu item selected $item") } } @@ -97,7 +94,7 @@ class SongMenuDialogFragment : MenuDialogFragment() { * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint -class AlbumMenuDialogFragment : MenuDialogFragment() { +class AlbumMenuDialogFragment : MenuDialogFragment() { override val menuModel: MenuViewModel by viewModels() override val listModel: ListViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels() @@ -105,38 +102,36 @@ class AlbumMenuDialogFragment : MenuDialogFragment() { private val playbackModel: PlaybackViewModel by activityViewModels() private val args: AlbumMenuDialogFragmentArgs by navArgs() - override val menuRes: Int - get() = args.menuRes - override val uid: Music.UID - get() = args.albumUid + override val parcel + get() = args.parcel // Nothing to disable in album menus. - override fun getDisabledItemIds(music: Album) = setOf() + override fun getDisabledItemIds(menu: Menu.ForAlbum) = setOf() - override fun updateMusic(binding: DialogMenuBinding, music: Album) { + override fun updateMenu(binding: DialogMenuBinding, menu: Menu.ForAlbum) { val context = requireContext() - binding.menuCover.bind(music) - binding.menuType.text = getString(music.releaseType.stringRes) - binding.menuName.text = music.name.resolve(context) - binding.menuInfo.text = music.artists.resolveNames(context) + binding.menuCover.bind(menu.album) + binding.menuType.text = getString(menu.album.releaseType.stringRes) + binding.menuName.text = menu.album.name.resolve(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) { - R.id.action_play -> playbackModel.play(music) - R.id.action_shuffle -> playbackModel.shuffle(music) - R.id.action_detail -> detailModel.showAlbum(music) + R.id.action_play -> playbackModel.play(menu.album) + R.id.action_shuffle -> playbackModel.shuffle(menu.album) + R.id.action_detail -> detailModel.showAlbum(menu.album) R.id.action_play_next -> { - playbackModel.playNext(music) + playbackModel.playNext(menu.album) requireContext().showToast(R.string.lng_queue_added) } R.id.action_queue_add -> { - playbackModel.addToQueue(music) + playbackModel.addToQueue(menu.album) requireContext().showToast(R.string.lng_queue_added) } - R.id.action_artist_details -> detailModel.showArtist(music) - R.id.action_playlist_add -> musicModel.addToPlaylist(music) - R.id.action_share -> requireContext().share(music) + R.id.action_artist_details -> detailModel.showArtist(menu.album) + R.id.action_playlist_add -> musicModel.addToPlaylist(menu.album) + R.id.action_share -> requireContext().share(menu.album) else -> error("Unexpected menu item selected $item") } } @@ -148,7 +143,7 @@ class AlbumMenuDialogFragment : MenuDialogFragment() { * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint -class ArtistMenuDialogFragment : MenuDialogFragment() { +class ArtistMenuDialogFragment : MenuDialogFragment() { override val menuModel: MenuViewModel by viewModels() override val listModel: ListViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels() @@ -156,13 +151,11 @@ class ArtistMenuDialogFragment : MenuDialogFragment() { private val playbackModel: PlaybackViewModel by activityViewModels() private val args: ArtistMenuDialogFragmentArgs by navArgs() - override val menuRes: Int - get() = args.menuRes - override val uid: Music.UID - get() = args.artistUid + override val parcel + get() = args.parcel - override fun getDisabledItemIds(music: Artist) = - if (music.songs.isEmpty()) { + override fun getDisabledItemIds(menu: Menu.ForArtist) = + if (menu.artist.songs.isEmpty()) { // Disable any operations that require some kind of songs to work with, as there won't // be any in an empty artist. setOf( @@ -176,37 +169,37 @@ class ArtistMenuDialogFragment : MenuDialogFragment() { setOf() } - override fun updateMusic(binding: DialogMenuBinding, music: Artist) { + override fun updateMenu(binding: DialogMenuBinding, menu: Menu.ForArtist) { val context = requireContext() - binding.menuCover.bind(music) + binding.menuCover.bind(menu.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 = getString( R.string.fmt_two, - context.getPlural(R.plurals.fmt_album_count, music.albums.size), - if (music.songs.isNotEmpty()) { - context.getPlural(R.plurals.fmt_song_count, music.songs.size) + context.getPlural(R.plurals.fmt_album_count, menu.artist.albums.size), + if (menu.artist.songs.isNotEmpty()) { + context.getPlural(R.plurals.fmt_song_count, menu.artist.songs.size) } else { getString(R.string.def_song_count) }) } - override fun onClick(item: MenuItem, music: Artist) { + override fun onClick(item: MenuItem, menu: Menu.ForArtist) { when (item.itemId) { - R.id.action_play -> playbackModel.play(music) - R.id.action_shuffle -> playbackModel.shuffle(music) - R.id.action_detail -> detailModel.showArtist(music) + R.id.action_play -> playbackModel.play(menu.artist) + R.id.action_shuffle -> playbackModel.shuffle(menu.artist) + R.id.action_detail -> detailModel.showArtist(menu.artist) R.id.action_play_next -> { - playbackModel.playNext(music) + playbackModel.playNext(menu.artist) requireContext().showToast(R.string.lng_queue_added) } R.id.action_queue_add -> { - playbackModel.addToQueue(music) + playbackModel.addToQueue(menu.artist) requireContext().showToast(R.string.lng_queue_added) } - R.id.action_playlist_add -> musicModel.addToPlaylist(music) - R.id.action_share -> requireContext().share(music) + R.id.action_playlist_add -> musicModel.addToPlaylist(menu.artist) + R.id.action_share -> requireContext().share(menu.artist) else -> error("Unexpected menu item $item") } } @@ -218,7 +211,7 @@ class ArtistMenuDialogFragment : MenuDialogFragment() { * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint -class GenreMenuDialogFragment : MenuDialogFragment() { +class GenreMenuDialogFragment : MenuDialogFragment() { override val menuModel: MenuViewModel by viewModels() override val listModel: ListViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels() @@ -226,40 +219,38 @@ class GenreMenuDialogFragment : MenuDialogFragment() { private val playbackModel: PlaybackViewModel by activityViewModels() private val args: GenreMenuDialogFragmentArgs by navArgs() - override val menuRes: Int - get() = args.menuRes - override val uid: Music.UID - get() = args.genreUid + override val parcel + get() = args.parcel - override fun getDisabledItemIds(music: Genre) = setOf() + override fun getDisabledItemIds(menu: Menu.ForGenre) = setOf() - override fun updateMusic(binding: DialogMenuBinding, music: Genre) { + override fun updateMenu(binding: DialogMenuBinding, menu: Menu.ForGenre) { val context = requireContext() - binding.menuCover.bind(music) + binding.menuCover.bind(menu.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 = getString( R.string.fmt_two, - context.getPlural(R.plurals.fmt_artist_count, music.artists.size), - context.getPlural(R.plurals.fmt_song_count, music.songs.size)) + context.getPlural(R.plurals.fmt_artist_count, menu.genre.artists.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) { - R.id.action_play -> playbackModel.play(music) - R.id.action_shuffle -> playbackModel.shuffle(music) - R.id.action_detail -> detailModel.showGenre(music) + R.id.action_play -> playbackModel.play(menu.genre) + R.id.action_shuffle -> playbackModel.shuffle(menu.genre) + R.id.action_detail -> detailModel.showGenre(menu.genre) R.id.action_play_next -> { - playbackModel.playNext(music) + playbackModel.playNext(menu.genre) requireContext().showToast(R.string.lng_queue_added) } R.id.action_queue_add -> { - playbackModel.addToQueue(music) + playbackModel.addToQueue(menu.genre) requireContext().showToast(R.string.lng_queue_added) } - R.id.action_playlist_add -> musicModel.addToPlaylist(music) - R.id.action_share -> requireContext().share(music) + R.id.action_playlist_add -> musicModel.addToPlaylist(menu.genre) + R.id.action_share -> requireContext().share(menu.genre) else -> error("Unexpected menu item $item") } } @@ -271,7 +262,7 @@ class GenreMenuDialogFragment : MenuDialogFragment() { * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint -class PlaylistMenuDialogFragment : MenuDialogFragment() { +class PlaylistMenuDialogFragment : MenuDialogFragment() { override val menuModel: MenuViewModel by viewModels() override val listModel: ListViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels() @@ -279,13 +270,11 @@ class PlaylistMenuDialogFragment : MenuDialogFragment() { private val playbackModel: PlaybackViewModel by activityViewModels() private val args: PlaylistMenuDialogFragmentArgs by navArgs() - override val menuRes: Int - get() = args.menuRes - override val uid: Music.UID - get() = args.playlistUid + override val parcel + get() = args.parcel - override fun getDisabledItemIds(music: Playlist) = - if (music.songs.isEmpty()) { + override fun getDisabledItemIds(menu: Menu.ForPlaylist) = + if (menu.playlist.songs.isEmpty()) { // Disable any operations that require some kind of songs to work with, as there won't // be any in an empty playlist. setOf( @@ -299,35 +288,35 @@ class PlaylistMenuDialogFragment : MenuDialogFragment() { setOf() } - override fun updateMusic(binding: DialogMenuBinding, music: Playlist) { + override fun updateMenu(binding: DialogMenuBinding, menu: Menu.ForPlaylist) { val context = requireContext() - binding.menuCover.bind(music) + binding.menuCover.bind(menu.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 = - if (music.songs.isNotEmpty()) { - context.getPlural(R.plurals.fmt_song_count, music.songs.size) + if (menu.playlist.songs.isNotEmpty()) { + context.getPlural(R.plurals.fmt_song_count, menu.playlist.songs.size) } else { getString(R.string.def_song_count) } } - override fun onClick(item: MenuItem, music: Playlist) { + override fun onClick(item: MenuItem, menu: Menu.ForPlaylist) { when (item.itemId) { - R.id.action_play -> playbackModel.play(music) - R.id.action_shuffle -> playbackModel.shuffle(music) - R.id.action_detail -> detailModel.showPlaylist(music) + R.id.action_play -> playbackModel.play(menu.playlist) + R.id.action_shuffle -> playbackModel.shuffle(menu.playlist) + R.id.action_detail -> detailModel.showPlaylist(menu.playlist) R.id.action_play_next -> { - playbackModel.playNext(music) + playbackModel.playNext(menu.playlist) requireContext().showToast(R.string.lng_queue_added) } R.id.action_queue_add -> { - playbackModel.addToQueue(music) + playbackModel.addToQueue(menu.playlist) requireContext().showToast(R.string.lng_queue_added) } - R.id.action_rename -> musicModel.renamePlaylist(music) - R.id.action_delete -> musicModel.deletePlaylist(music) - R.id.action_share -> requireContext().share(music) + R.id.action_rename -> musicModel.renamePlaylist(menu.playlist) + R.id.action_delete -> musicModel.deletePlaylist(menu.playlist) + R.id.action_share -> requireContext().share(menu.playlist) else -> error("Unexpected menu item $item") } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuItemAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuItemAdapter.kt index 1fd336c34..d5b5158f4 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuItemAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuItemAdapter.kt @@ -44,7 +44,7 @@ class MenuItemAdapter(private val listener: ClickableListListener) : } /** - * A [DialogRecyclerView.ViewHolder] that displays a list of menu options based on [MenuItem]. + * A [DialogRecyclerView.ViewHolder] that displays a [MenuItem]. * * @author Alexander Capehart (OxygenCobalt) */ @@ -54,7 +54,7 @@ class MenuItemViewHolder private constructor(private val binding: ItemMenuOption * Bind new data to this instance. * * @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) { listener.bind(item, this) diff --git a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt index 3e34c1a52..0d5388854 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/menu/MenuViewModel.kt @@ -23,8 +23,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow 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.playback.PlaySong import org.oxycblt.auxio.util.logW /** @@ -35,32 +36,62 @@ import org.oxycblt.auxio.util.logW @HiltViewModel class MenuViewModel @Inject constructor(private val musicRepository: MusicRepository) : ViewModel(), MusicRepository.UpdateListener { - private val _currentMusic = MutableStateFlow(null) - /** The current [Music] information being shown in a menu dialog. */ - val currentMusic: StateFlow = _currentMusic + private val _currentMenu = MutableStateFlow(null) + /** The current [Menu] information being shown in a dialog. */ + val currentMenu: StateFlow = _currentMenu init { musicRepository.addUpdateListener(this) } 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() { musicRepository.removeUpdateListener(this) } - /** - * Set a new [currentMusic] from it's [Music.UID]. [currentMusic] will be updated to align with - * the new album. - * - * @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") + fun setMenu(parcel: Menu.Parcel) { + _currentMenu.value = unpackParcel(parcel) + if (_currentMenu.value == null) { + logW("Given menu parcel $parcel 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) + } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt index 90a5786f6..9fc255ce1 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt @@ -22,7 +22,6 @@ import android.content.Context import android.util.AttributeSet import android.view.View import android.view.ViewGroup -import android.view.WindowInsets import androidx.annotation.AttrRes import androidx.core.view.isInvisible 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.list.recycler.DialogRecyclerView.ViewHolder import org.oxycblt.auxio.util.getDimenPixels -import org.oxycblt.auxio.util.systemBarInsetsCompat /** * A [RecyclerView] intended for use in Dialogs, adding features such as: @@ -76,13 +74,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr 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) { super.onScrolled(dx, dy) // Scroll event occurred, need to update the dividers. diff --git a/app/src/main/java/org/oxycblt/auxio/list/Sort.kt b/app/src/main/java/org/oxycblt/auxio/list/sort/Sort.kt similarity index 88% rename from app/src/main/java/org/oxycblt/auxio/list/Sort.kt rename to app/src/main/java/org/oxycblt/auxio/list/sort/Sort.kt index 8a7203182..d4088358a 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/sort/Sort.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Auxio Project + * Copyright (c) 2023 Auxio Project * Sort.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify @@ -16,9 +16,8 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.list +package org.oxycblt.auxio.list.sort -import androidx.annotation.IdRes import kotlin.math.max import org.oxycblt.auxio.IntegerTable 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.Genre import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.info.Date @@ -42,22 +40,6 @@ import org.oxycblt.auxio.music.info.Disc * @author Alexander Capehart (OxygenCobalt) */ 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. * @@ -163,8 +145,8 @@ data class Sort(val mode: Mode, val direction: Direction) { sealed interface Mode { /** The integer representation of this sort mode. */ val intCode: Int - /** The item ID of this sort mode in menu resources. */ - val itemId: Int + /** The string resource of the human-readable name of this sort mode. */ + val stringRes: Int /** * 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 */ - object ByName : Mode { + data object ByName : Mode { override val intCode: Int get() = IntegerTable.SORT_BY_NAME - override val itemId: Int - get() = R.id.option_sort_name + override val stringRes: Int + get() = R.string.lbl_name override fun getSongComparator(direction: Direction) = compareByDynamic(direction, BasicComparator.SONG) @@ -244,12 +226,12 @@ data class Sort(val mode: Mode, val direction: Direction) { * * @see Album.name */ - object ByAlbum : Mode { + data object ByAlbum : Mode { override val intCode: Int get() = IntegerTable.SORT_BY_ALBUM - override val itemId: Int - get() = R.id.option_sort_album + override val stringRes: Int + get() = R.string.lbl_album override fun getSongComparator(direction: Direction): Comparator = MultiComparator( @@ -264,12 +246,12 @@ data class Sort(val mode: Mode, val direction: Direction) { * * @see Artist.name */ - object ByArtist : Mode { + data object ByArtist : Mode { override val intCode: Int get() = IntegerTable.SORT_BY_ARTIST - override val itemId: Int - get() = R.id.option_sort_artist + override val stringRes: Int + get() = R.string.lbl_artist override fun getSongComparator(direction: Direction): Comparator = MultiComparator( @@ -293,12 +275,12 @@ data class Sort(val mode: Mode, val direction: Direction) { * @see Song.date * @see Album.dates */ - object ByDate : Mode { + data object ByDate : Mode { override val intCode: Int get() = IntegerTable.SORT_BY_YEAR - override val itemId: Int - get() = R.id.option_sort_year + override val stringRes: Int + get() = R.string.lbl_date override fun getSongComparator(direction: Direction): Comparator = MultiComparator( @@ -315,12 +297,12 @@ data class Sort(val mode: Mode, val direction: Direction) { } /** Sort by the duration of an item. */ - object ByDuration : Mode { + data object ByDuration : Mode { override val intCode: Int get() = IntegerTable.SORT_BY_DURATION - override val itemId: Int - get() = R.id.option_sort_duration + override val stringRes: Int + get() = R.string.lbl_duration override fun getSongComparator(direction: Direction): Comparator = MultiComparator( @@ -345,17 +327,13 @@ data class Sort(val mode: Mode, val direction: Direction) { compareBy(BasicComparator.PLAYLIST)) } - /** - * Sort by the amount of songs an item contains. Only available for [MusicParent]s. - * - * @see MusicParent.songs - */ - object ByCount : Mode { + /** Sort by the amount of songs an item contains. Only available for MusicParents. */ + data object ByCount : Mode { override val intCode: Int get() = IntegerTable.SORT_BY_COUNT - override val itemId: Int - get() = R.id.option_sort_count + override val stringRes: Int + get() = R.string.lbl_song_count override fun getAlbumComparator(direction: Direction): Comparator = MultiComparator( @@ -381,12 +359,12 @@ data class Sort(val mode: Mode, val direction: Direction) { * * @see Song.disc */ - object ByDisc : Mode { + data object ByDisc : Mode { override val intCode: Int get() = IntegerTable.SORT_BY_DISC - override val itemId: Int - get() = R.id.option_sort_disc + override val stringRes: Int + get() = R.string.lbl_disc override fun getSongComparator(direction: Direction): Comparator = MultiComparator( @@ -400,12 +378,12 @@ data class Sort(val mode: Mode, val direction: Direction) { * * @see Song.track */ - object ByTrack : Mode { + data object ByTrack : Mode { override val intCode: Int get() = IntegerTable.SORT_BY_TRACK - override val itemId: Int - get() = R.id.option_sort_track + override val stringRes: Int + get() = R.string.lbl_track override fun getSongComparator(direction: Direction): Comparator = MultiComparator( @@ -420,12 +398,12 @@ data class Sort(val mode: Mode, val direction: Direction) { * @see Song.dateAdded * @see Album.dates */ - object ByDateAdded : Mode { + data object ByDateAdded : Mode { override val intCode: Int get() = IntegerTable.SORT_BY_DATE_ADDED - override val itemId: Int - get() = R.id.option_sort_date_added + override val stringRes: Int + get() = R.string.lbl_date_added override fun getSongComparator(direction: Direction): Comparator = MultiComparator( @@ -458,27 +436,6 @@ data class Sort(val mode: Mode, val direction: Direction) { ByDateAdded.intCode -> ByDateAdded 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 - } } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/sort/SortDialog.kt b/app/src/main/java/org/oxycblt/auxio/list/sort/SortDialog.kt new file mode 100644 index 000000000..8bdb31924 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/list/sort/SortDialog.kt @@ -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 . + */ + +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(), + ClickableListListener, + MaterialButtonToggleGroup.OnButtonCheckedListener { + private val modeAdapter = SortModeAdapter(@Suppress("LeakingThis") this) + + abstract fun getInitialSort(): Sort? + + abstract fun applyChosenSort(sort: Sort) + + abstract fun getModeChoices(): List + + 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) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/list/sort/SortModeAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/sort/SortModeAdapter.kt new file mode 100644 index 000000000..25a8d2c69 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/list/sort/SortModeAdapter.kt @@ -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 . + */ + +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) : + FlexibleListAdapter(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) { + 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) { + 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() { + override fun areItemsTheSame(oldItem: Sort.Mode, newItem: Sort.Mode) = + oldItem == newItem + + override fun areContentsTheSame(oldItem: Sort.Mode, newItem: Sort.Mode) = + oldItem == newItem + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/Indexing.kt b/app/src/main/java/org/oxycblt/auxio/music/Indexing.kt index 545a5f234..a185d5b2f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Indexing.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Indexing.kt @@ -57,7 +57,7 @@ sealed interface IndexingState { */ sealed interface IndexingProgress { /** Other work is being done that does not have a defined progress. */ - object Indeterminate : IndexingProgress + data object Indeterminate : IndexingProgress /** * Songs are currently being loaded. diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index fc8a51390..766ea462c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -80,23 +80,23 @@ sealed interface Music : Item { class UID private constructor( private val format: Format, - private val mode: MusicMode, + private val type: MusicType, private val uuid: UUID ) : Parcelable { // Cache the hashCode for HashMap efficiency. @IgnoredOnParcel private var hashCode = format.hashCode() init { - hashCode = 31 * hashCode + mode.hashCode() + hashCode = 31 * hashCode + type.hashCode() hashCode = 31 * hashCode + uuid.hashCode() } override fun hashCode() = hashCode 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. @@ -124,23 +124,23 @@ sealed interface Music : Item { * Creates an Auxio-style [UID] of random composition. Used if there is no * 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 { - return UID(Format.AUXIO, mode, UUID.randomUUID()) + fun auxio(type: MusicType): UID { + return UID(Format.AUXIO, type, UUID.randomUUID()) } /** * Creates an Auxio-style [UID] with a [UUID] composed of a hash of the 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]. * @param updates Block to update the [MessageDigest] hash with the metadata of the * item. Make sure the metadata hashed semantically aligns with the format * specification. * @return A new auxio-style [UID]. */ - fun auxio(mode: MusicMode, updates: MessageDigest.() -> Unit): UID { + fun auxio(type: MusicType, updates: MessageDigest.() -> Unit): UID { val digest = MessageDigest.getInstance("SHA-256").run { updates() @@ -170,19 +170,19 @@ sealed interface Music : Item { .or(digest[13].toLong().and(0xFF).shl(16)) .or(digest[14].toLong().and(0xFF).shl(8)) .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 * 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 * file. * @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. @@ -210,10 +210,10 @@ sealed interface Music : Item { return null } - val mode = - MusicMode.fromIntCode(ids[0].toIntOrNull(16) ?: return null) ?: return null + val type = + MusicType.fromIntCode(ids[0].toIntOrNull(16) ?: return null) ?: return null val uuid = ids[1].toUuidOrNull() ?: return null - return UID(format, mode, uuid) + return UID(format, type, uuid) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt index e875dd04b..b7e9d3b0d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt @@ -28,5 +28,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) interface MusicModule { @Singleton @Binds fun repository(musicRepository: MusicRepositoryImpl): MusicRepository + @Binds fun settings(musicSettingsImpl: MusicSettingsImpl): MusicSettings } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt index 4274ef4e8..f2930d3ec 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt @@ -24,7 +24,6 @@ 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 import org.oxycblt.auxio.music.fs.Directory import org.oxycblt.auxio.music.fs.MusicDirectories import org.oxycblt.auxio.settings.Settings @@ -47,23 +46,6 @@ interface MusicSettings : Settings { var multiValueSeparators: String /** Whether to enable more advanced sorting by articles and numbers. */ 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 { /** 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 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) { // TODO: Differentiate "hard reloads" (Need the cache) and "Soft reloads" // (just need to manipulate data) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicType.kt similarity index 73% rename from app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt rename to app/src/main/java/org/oxycblt/auxio/music/MusicType.kt index 03ec48dae..19f535af1 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicType.kt @@ -1,6 +1,6 @@ /* * 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 * 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 /** - * 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) */ -enum class MusicMode { - /** Configure with respect to [Song] instances. */ +enum class MusicType { + /** @see Song */ SONGS, - /** Configure with respect to [Album] instances. */ + /** @see Album */ ALBUMS, - /** Configure with respect to [Artist] instances. */ + /** @see Artist */ ARTISTS, - /** Configure with respect to [Genre] instances. */ + /** @see Genre */ GENRES, - /** Configure with respect to [Playlist] instances. */ + /** @see Playlist */ PLAYLISTS; /** @@ -54,11 +54,11 @@ enum class MusicMode { 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] - * @return The corresponding [MusicMode], or null if the [MusicMode] is invalid. - * @see MusicMode.intCode + * @param intCode An integer representation of a [MusicType] + * @return The corresponding [MusicType], or null if the [MusicType] is invalid. + * @see MusicType.intCode */ fun fromIntCode(intCode: Int) = when (intCode) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt index 6e314a4f1..314b38785 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import org.oxycblt.auxio.list.ListSettings import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent import org.oxycblt.auxio.util.logD @@ -39,8 +40,8 @@ import org.oxycblt.auxio.util.logD class MusicViewModel @Inject constructor( + private val listSettings: ListSettings, private val musicRepository: MusicRepository, - private val musicSettings: MusicSettings ) : ViewModel(), MusicRepository.UpdateListener, MusicRepository.IndexingListener { private val _indexingState = MutableStateFlow(null) @@ -167,7 +168,7 @@ constructor( */ fun addToPlaylist(album: Album, playlist: Playlist? = null) { 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) { 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) { logD("Adding $genre to playlist") - addToPlaylist(musicSettings.genreSongSort.songs(genre.songs), playlist) + addToPlaylist(listSettings.genreSongSort.songs(genre.songs), playlist) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt index b1a19d52a..d28547239 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt @@ -40,7 +40,9 @@ abstract class CacheDatabase : RoomDatabase() { @Dao interface CachedSongsDao { @Query("SELECT * FROM CachedSong") suspend fun readSongs(): List + @Query("DELETE FROM CachedSong") suspend fun nukeSongs() + @Insert suspend fun insertSongs(songs: List) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt index 0b91c65b3..441a9a7fa 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt @@ -100,6 +100,7 @@ private class CacheImpl(cachedSongs: List) : Cache { } override var invalidated = false + override fun populate(rawSong: RawSong): Boolean { // 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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/decision/PlaylistPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/decision/PlaylistPickerViewModel.kt index def23d977..b21d9e991 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/decision/PlaylistPickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/decision/PlaylistPickerViewModel.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.R 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.MusicRepository import org.oxycblt.auxio.music.Playlist @@ -260,9 +260,9 @@ sealed interface ChosenName { /** The current name already exists. */ data class AlreadyExists(val prior: String) : ChosenName /** The current name is empty. */ - object Empty : ChosenName + data object Empty : ChosenName /** The current name only consists of whitespace. */ - object Blank : ChosenName + data object Blank : ChosenName } /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index 8032c46d7..cd75ba578 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -75,7 +75,7 @@ interface DeviceLibrary { * Find a [Album] instance corresponding to the given [Music.UID]. * * @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? @@ -83,7 +83,7 @@ interface DeviceLibrary { * Find a [Artist] instance corresponding to the given [Music.UID]. * * @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? @@ -91,7 +91,7 @@ interface DeviceLibrary { * Find a [Genre] instance corresponding to the given [Music.UID]. * * @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? @@ -266,14 +266,19 @@ class DeviceLibraryImpl( // 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 hashCode() = songs.hashCode() + override fun toString() = "DeviceLibrary(songs=${songs.size}, albums=${albums.size}, " + "artists=${artists.size}, genres=${genres.size})" override fun findSong(uid: Music.UID): Song? = songUidMap[uid] + override fun findAlbum(uid: Music.UID): Album? = albumUidMap[uid] + override fun findArtist(uid: Music.UID): Artist? = artistUidMap[uid] + override fun findGenre(uid: Music.UID): Genre? = genreUidMap[uid] override fun findSongForUri(context: Context, uri: Uri) = diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index 2f12b7290..1d2ce2a26 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -20,13 +20,13 @@ package org.oxycblt.auxio.music.device import org.oxycblt.auxio.R 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.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicSettings +import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.fs.MimeType 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 { override val 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) } - ?: Music.UID.auxio(MusicMode.SONGS) { + rawSong.musicBrainzId?.toUuidOrNull()?.let { Music.UID.musicBrainz(MusicType.SONGS, it) } + ?: Music.UID.auxio(MusicType.SONGS) { // 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 // 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() override fun hashCode() = hashCode + override fun equals(other: Any?) = other is SongImpl && uid == other.uid && rawSong == other.rawSong + override fun toString() = "Song(uid=$uid, name=$name)" private val artistMusicBrainzIds = rawSong.artistMusicBrainzIds.parseMultiValue(musicSettings) @@ -251,8 +253,8 @@ class AlbumImpl( override val uid = // Attempt to use a MusicBrainz ID first before falling back to a hashed UID. - rawAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ALBUMS, it) } - ?: Music.UID.auxio(MusicMode.ALBUMS) { + rawAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicType.ALBUMS, it) } + ?: Music.UID.auxio(MusicType.ALBUMS) { // 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 // 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 equals(other: Any?) = other is AlbumImpl && uid == other.uid && rawAlbum == other.rawAlbum && songs == other.songs + override fun toString() = "Album(uid=$uid, name=$name)" /** @@ -366,8 +370,8 @@ class ArtistImpl(grouping: Grouping, musicSettings: MusicSetti override val uid = // Attempt to use a MusicBrainz ID first before falling back to a hashed UID. - rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) } - ?: Music.UID.auxio(MusicMode.ARTISTS) { update(rawArtist.name) } + rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicType.ARTISTS, it) } + ?: Music.UID.auxio(MusicType.ARTISTS) { update(rawArtist.name) } override val name = rawArtist.name?.let { Name.Known.from(it, rawArtist.sortName, musicSettings) } ?: Name.Unknown(R.string.def_artist) @@ -461,7 +465,7 @@ class ArtistImpl(grouping: Grouping, musicSettings: MusicSetti class GenreImpl(grouping: Grouping, musicSettings: MusicSettings) : Genre { 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 = rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) } ?: Name.Unknown(R.string.def_genre) diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt index 11a357f45..76e62e897 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt @@ -74,9 +74,13 @@ interface MediaStoreExtractor { /** A black-box interface representing a query from the media database. */ interface Query { val projectedTotal: Int + fun moveToNext(): Boolean + fun close() + fun populateFileInfo(rawSong: RawSong) + fun populateTags(rawSong: RawSong) } @@ -285,7 +289,9 @@ private abstract class BaseMediaStoreExtractor( private val albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST) final override val projectedTotal = cursor.count + final override fun moveToNext() = cursor.moveToNext() + final override fun close() = cursor.close() override fun populateFileInfo(rawSong: RawSong) { @@ -524,6 +530,7 @@ private class Api29MediaStoreExtractor(context: Context, musicSettings: MusicSet storageManager: StorageManager ) : BaseApi29MediaStoreExtractor.Query(cursor, genreNamesMap, storageManager) { private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) + override fun populateTags(rawSong: RawSong) { super.populateTags(rawSong) // This extractor is volume-aware, but does not support the modern track columns. diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/StorageUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/StorageUtil.kt index 1a057bd94..87eff7081 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/StorageUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/StorageUtil.kt @@ -83,7 +83,7 @@ inline fun ContentResolver.useQuery( ) = safeQuery(uri, projection, selector, args).use(block) /** 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. @@ -102,21 +102,11 @@ fun Long.toAudioUri() = * @return An external storage image [Uri]. May not exist. * @see ContentUris.withAppendedId */ -fun Long.toCoverUri() = ContentUris.withAppendedId(EXTERNAL_COVERS_URI, this) +fun Long.toCoverUri() = ContentUris.withAppendedId(externalCoversUri, this) // --- STORAGEMANAGER UTILITIES --- // 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 * 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 */ @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 @@ -143,13 +133,7 @@ val StorageManager.primaryStorageVolumeCompat: StorageVolume * @see StorageManager.getStorageVolumes */ val StorageManager.storageVolumesCompat: List - get() = - 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).toList() - } + get() = storageVolumes.toList() /** * 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 when (stateCompat) { Environment.MEDIA_MOUNTED, - Environment.MEDIA_MOUNTED_READ_ONLY -> - SV_API21_GET_PATH_METHOD.invoke(this) as String + Environment.MEDIA_MOUNTED_READ_ONLY -> svApi21GetPathMethod.invoke(this) as String else -> null } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt index 783b1356a..4eb8969f4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt @@ -74,8 +74,11 @@ class Date private constructor(private val tokens: List) : Comparable } override fun equals(other: Any?) = other is Date && compareTo(other) == 0 + override fun hashCode() = tokens.hashCode() + override fun toString() = StringBuilder().appendDate().toString() + override fun compareTo(other: Date): Int { for (i in 0 until max(tokens.size, other.tokens.size)) { val ai = tokens.getOrNull(i) diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt index 52b7ab646..2c8fd360b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt @@ -29,6 +29,8 @@ import org.oxycblt.auxio.list.Item class Disc(val number: Int, val name: String?) : Item, Comparable { // 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 hashCode() = number.hashCode() + override fun compareTo(other: Disc) = number.compareTo(other.number) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt index 03f3a33f6..bbde5aca3 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt @@ -132,7 +132,9 @@ sealed interface Name : Comparable { */ data class Unknown(@StringRes val stringRes: Int) : Name { override val thumb = "?" + override fun resolve(context: Context) = context.getString(stringRes) + override fun compareTo(other: Name) = when (other) { // Unknown names do not need any direct comparison right now. @@ -143,8 +145,8 @@ sealed interface Name : Comparable { } } -private val COLLATOR: Collator = Collator.getInstance().apply { strength = Collator.PRIMARY } -private val PUNCT_REGEX by lazy { Regex("[\\p{Punct}+]") } +private val collator: Collator = Collator.getInstance().apply { strength = Collator.PRIMARY } +private val punctRegex by lazy { Regex("[\\p{Punct}+]") } /** * 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 { // Remove excess punctuation from the string, as those usually aren't considered in sorting. - val stripped = name.replace(PUNCT_REGEX, "").ifEmpty { name } - val collationKey = COLLATOR.getCollationKey(stripped) + val stripped = name.replace(punctRegex, "").ifEmpty { name } + val collationKey = collator.getCollationKey(stripped) // Always use lexicographic mode since we aren't parsing any numeric components return SortToken(collationKey, SortToken.Type.LEXICOGRAPHIC) } @@ -179,7 +181,7 @@ private data class IntelligentKnownName(override val raw: String, override val s val stripped = name // Remove excess punctuation from the string, as those u - .replace(PUNCT_REGEX, "") + .replace(punctRegex, "") .ifEmpty { name } .run { // 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 = token.trimStart { Character.getNumericValue(it) == 0 }.ifEmpty { token } // Other languages have other types of digit strings, still use collation keys - collationKey = COLLATOR.getCollationKey(digits) + collationKey = collator.getCollationKey(digits) type = SortToken.Type.NUMERIC } else { - collationKey = COLLATOR.getCollationKey(token) + collationKey = collator.getCollationKey(token) type = SortToken.Type.LEXICOGRAPHIC } SortToken(collationKey, type) diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/ReleaseType.kt b/app/src/main/java/org/oxycblt/auxio/music/info/ReleaseType.kt index c4cb00fd3..24260912b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/ReleaseType.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/ReleaseType.kt @@ -111,7 +111,7 @@ sealed interface ReleaseType { * A soundtrack. Similar to a [Compilation], but created for a specific piece of (usually * visual) media. */ - object Soundtrack : ReleaseType { + data object Soundtrack : ReleaseType { override val refinement: Refinement? get() = null @@ -123,7 +123,7 @@ sealed interface ReleaseType { * A (DJ) Mix. These are usually one large track consisting of the artist playing several * sub-tracks with smooth transitions between them. */ - object Mix : ReleaseType { + data object Mix : ReleaseType { override val refinement: Refinement? 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 * future release. */ - object Mixtape : ReleaseType { + data object Mixtape : ReleaseType { override val refinement: Refinement? get() = null diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/MetadataModule.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/MetadataModule.kt index 96685a746..1f42df505 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/MetadataModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/MetadataModule.kt @@ -27,6 +27,8 @@ import dagger.hilt.components.SingletonComponent @InstallIn(SingletonComponent::class) interface MetadataModule { @Binds fun tagExtractor(extractor: TagExtractorImpl): TagExtractor + @Binds fun tagWorkerFactory(factory: TagWorkerFactoryImpl): TagWorker.Factory + @Binds fun audioPropertiesFactory(factory: AudioPropertiesFactoryImpl): AudioProperties.Factory } diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagUtil.kt index ebc5b6bc6..b5e5e131e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagUtil.kt @@ -204,14 +204,14 @@ private fun String.parseId3v1Genre(): String? { "RX" -> "Remix" else -> null } - return GENRE_TABLE.getOrNull(numeric) + return genreTable.getOrNull(numeric) } /** * A [Regex] that implements parsing for ID3v2's genre format. Derived from 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 @@ -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. */ private fun String.parseId3v2Genre(): List? { - val groups = (ID3V2_GENRE_RE.matchEntire(this) ?: return null).groupValues + val groups = (id3v2GenreRe.matchEntire(this) ?: return null).groupValues val genres = mutableSetOf() // ID3v2.3 genres are far more complex and require string grokking to properly implement. @@ -260,7 +260,7 @@ private fun String.parseId3v2Genre(): List? { * A table of the "conventional" mapping between ID3v1 integer genres and their named counterparts. * Includes non-standard extensions. */ -private val GENRE_TABLE = +private val genreTable = arrayOf( // ID3 Standard "Blues", diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt index 301fa1b46..e94e4fe16 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt @@ -36,7 +36,7 @@ import org.oxycblt.auxio.util.newMainPendingIntent * @author Alexander Capehart (OxygenCobalt) */ class IndexingNotification(private val context: Context) : - ForegroundServiceNotification(context, INDEXER_CHANNEL) { + ForegroundServiceNotification(context, indexerChannel) { private var lastUpdateTime = -1L init { @@ -98,7 +98,7 @@ class IndexingNotification(private val context: Context) : * @author Alexander Capehart (OxygenCobalt) */ class ObservingNotification(context: Context) : - ForegroundServiceNotification(context, INDEXER_CHANNEL) { + ForegroundServiceNotification(context, indexerChannel) { init { setSmallIcon(R.drawable.ic_indexer_24) setCategory(NotificationCompat.CATEGORY_SERVICE) @@ -115,6 +115,6 @@ class ObservingNotification(context: Context) : } /** Notification channel shared by [IndexingNotification] and [ObservingNotification]. */ -private val INDEXER_CHANNEL = +private val indexerChannel = ForegroundServiceNotification.ChannelInfo( id = BuildConfig.APPLICATION_ID + ".channel.INDEXER", nameRes = R.string.lbl_indexer) diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt index 9ad14f411..ffe7a5174 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt @@ -19,8 +19,8 @@ package org.oxycblt.auxio.music.user import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicSettings +import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.device.DeviceLibrary @@ -42,7 +42,9 @@ private constructor( override fun equals(other: Any?) = other is PlaylistImpl && uid == other.uid && name == other.name && songs == other.songs + override fun hashCode() = hashCode + override fun toString() = "Playlist(uid=$uid, name=$name)" /** @@ -78,7 +80,7 @@ private constructor( */ fun from(name: String, songs: List, musicSettings: MusicSettings) = PlaylistImpl( - Music.UID.auxio(MusicMode.PLAYLISTS), + Music.UID.auxio(MusicType.PLAYLISTS), Name.Known.from(name, null, musicSettings), songs) diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt index 5e0e7ca55..06de6d64f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt @@ -171,7 +171,9 @@ private class UserLibraryImpl( private val musicSettings: MusicSettings ) : MutableUserLibrary { override fun hashCode() = playlistMap.hashCode() + override fun equals(other: Any?) = other is UserLibraryImpl && other.playlistMap == playlistMap + override fun toString() = "UserLibrary(playlists=${playlists.size})" override val playlists: Collection diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt index 9c46bbe78..b6358ee3b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt @@ -51,7 +51,7 @@ abstract class UserMusicDatabase : RoomDatabase() { * @author Alexander Capehart (OxygenCobalt) */ @Dao -interface PlaylistDao { +abstract class PlaylistDao { /** * Read out all playlists stored in the database. * @@ -59,7 +59,7 @@ interface PlaylistDao { */ @Transaction @Query("SELECT * FROM PlaylistInfo") - suspend fun readRawPlaylists(): List + abstract suspend fun readRawPlaylists(): List /** * Create a new playlist. @@ -67,7 +67,7 @@ interface PlaylistDao { * @param rawPlaylist The [RawPlaylist] to create. */ @Transaction - suspend fun insertPlaylist(rawPlaylist: RawPlaylist) { + open suspend fun insertPlaylist(rawPlaylist: RawPlaylist) { insertInfo(rawPlaylist.playlistInfo) insertSongs(rawPlaylist.songs) insertRefs( @@ -83,7 +83,7 @@ interface PlaylistDao { * @param playlistInfo The new [PlaylistInfo] to store. */ @Transaction - suspend fun replacePlaylistInfo(playlistInfo: PlaylistInfo) { + open suspend fun replacePlaylistInfo(playlistInfo: PlaylistInfo) { deleteInfo(playlistInfo.playlistUid) insertInfo(playlistInfo) } @@ -94,7 +94,7 @@ interface PlaylistDao { * @param playlistUid The [Music.UID] of the playlist to delete. */ @Transaction - suspend fun deletePlaylist(playlistUid: Music.UID) { + open suspend fun deletePlaylist(playlistUid: Music.UID) { deleteInfo(playlistUid) deleteRefs(playlistUid) } @@ -106,7 +106,7 @@ interface PlaylistDao { * @param songs The [PlaylistSong] representing each song to put into the playlist. */ @Transaction - suspend fun insertPlaylistSongs(playlistUid: Music.UID, songs: List) { + open suspend fun insertPlaylistSongs(playlistUid: Music.UID, songs: List) { insertSongs(songs) insertRefs( songs.map { PlaylistSongCrossRef(playlistUid = playlistUid, songUid = it.songUid) }) @@ -120,7 +120,7 @@ interface PlaylistDao { * playlist. */ @Transaction - suspend fun replacePlaylistSongs(playlistUid: Music.UID, songs: List) { + open suspend fun replacePlaylistSongs(playlistUid: Music.UID, songs: List) { deleteRefs(playlistUid) insertSongs(songs) insertRefs( @@ -128,21 +128,22 @@ interface PlaylistDao { } /** 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. */ @Query("DELETE FROM PlaylistInfo where playlistUid = :playlistUid") - suspend fun deleteInfo(playlistUid: Music.UID) + abstract suspend fun deleteInfo(playlistUid: Music.UID) /** Internal, do not use. */ @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertSongs(songs: List) + abstract suspend fun insertSongs(songs: List) /** Internal, do not use. */ @Insert(onConflict = OnConflictStrategy.ABORT) - suspend fun insertRefs(refs: List) + abstract suspend fun insertRefs(refs: List) /** Internal, do not use. */ @Query("DELETE FROM PlaylistSongCrossRef where playlistUid = :playlistUid") - suspend fun deleteRefs(playlistUid: Music.UID) + abstract suspend fun deleteRefs(playlistUid: Music.UID) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaySong.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaySong.kt new file mode 100644 index 000000000..34dd90929 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaySong.kt @@ -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 . + */ + +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 + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackModule.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackModule.kt index 36fb9a0ee..37e3eb4c2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackModule.kt @@ -32,5 +32,6 @@ interface PlaybackModule { @Singleton @Binds fun stateManager(playbackManager: PlaybackStateManagerImpl): PlaybackStateManager + @Binds fun settings(playbackSettings: PlaybackSettingsImpl): PlaybackSettings } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 55f17ec3f..77736a3f3 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -39,8 +39,8 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.detail.Show +import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.music.MusicParent -import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.pager.PlaybackPageListener 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.lazyReflectedField 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.systemBarInsetsCompat @@ -71,9 +71,9 @@ class PlaybackPanelFragment : StyledSeekBar.Listener, PlaybackPageListener { private val playbackModel: PlaybackViewModel by activityViewModels() - private val musicModel: MusicViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels() private val queueModel: QueueViewModel by activityViewModels() + private val listModel: ListViewModel by activityViewModels() private var equalizerLauncher: ActivityResultLauncher? = null private var coverAdapter: PlaybackPagerAdapter? = null @@ -103,6 +103,13 @@ class PlaybackPanelFragment : binding.playbackToolbar.apply { setNavigationOnClickListener { playbackModel.openMain() } 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 @@ -142,53 +149,30 @@ class PlaybackPanelFragment : binding.playbackToolbar.setOnMenuItemClickListener(null) } - override fun onMenuItemClick(item: MenuItem) = - when (item.itemId) { - R.id.action_open_equalizer -> { - // Launch the system equalizer app, if possible. - logD("Launching equalizer") - val equalizerIntent = - Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL) - // Provide audio session ID so the equalizer can show options for this app - // in particular. - .putExtra( - AudioEffect.EXTRA_AUDIO_SESSION, playbackModel.currentAudioSessionId) - // Signal music type so that the equalizer settings are appropriate for - // music playback. - .putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC) - try { - requireNotNull(equalizerLauncher) { - "Equalizer panel launcher was not available" - } - .launch(equalizerIntent) - } catch (e: ActivityNotFoundException) { - requireContext().showToast(R.string.err_no_app) - } - true + override fun onMenuItemClick(item: MenuItem): Boolean { + if (item.itemId == R.id.action_open_equalizer) { + // Launch the system equalizer app, if possible. + logD("Launching equalizer") + val equalizerIntent = + Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL) + // Provide audio session ID so the equalizer can show options for this app + // in particular. + .putExtra(AudioEffect.EXTRA_AUDIO_SESSION, playbackModel.currentAudioSessionId) + // Signal music type so that the equalizer settings are appropriate for + // music playback. + .putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC) + try { + requireNotNull(equalizerLauncher) { "Equalizer panel launcher was not available" } + .launch(equalizerIntent) + } catch (e: ActivityNotFoundException) { + requireContext().showToast(R.string.err_no_app) } - R.id.action_artist_details -> { - navigateToCurrentArtist() - 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 + return true } + return false + } + override fun onSeekConfirmed(positionDs: Long) { playbackModel.seekTo(positionDs) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt index bfd97dbd4..a270c5c07 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt @@ -24,7 +24,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R -import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.playback.replaygain.ReplayGainMode import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp import org.oxycblt.auxio.settings.Settings @@ -46,16 +45,13 @@ interface PlaybackSettings : Settings { val replayGainMode: ReplayGainMode /** The current ReplayGain pre-amp configuration. */ 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 - * if to play from all Songs. + * How to play a song from a parent item, specified by [PlaySong]. Null if to delegate to the UI + * context. */ - val inListPlaybackMode: MusicMode - /** - * 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? + val inParentPlaybackMode: PlaySong? /** Whether to keep shuffle on when playing a new Song. */ val keepShuffle: Boolean /** Whether to rewind when the skip previous button is pressed before skipping back. */ @@ -75,18 +71,18 @@ interface PlaybackSettings : Settings { class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Context) : Settings.Impl(context), PlaybackSettings { - override val inListPlaybackMode: MusicMode + override val playInListWith: PlaySong get() = - MusicMode.fromIntCode( + PlaySong.fromIntCode( sharedPreferences.getInt( - getString(R.string.set_key_in_list_playback_mode), Int.MIN_VALUE)) - ?: MusicMode.SONGS + getString(R.string.set_key_play_in_list_with), Int.MIN_VALUE)) + ?: PlaySong.FromAll - override val inParentPlaybackMode: MusicMode? + override val inParentPlaybackMode: PlaySong? get() = - MusicMode.fromIntCode( + PlaySong.fromIntCode( 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 get() = @@ -132,65 +128,44 @@ class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Cont get() = sharedPreferences.getBoolean(getString(R.string.set_key_repeat_pause), false) override fun migrate() { - // "Use alternate notification action" was converted to an ActionMode setting in 3.0.0. - if (sharedPreferences.contains(OLD_KEY_ALT_NOTIF_ACTION)) { - 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() = + // MusicMode was converted to PlaySong in 3.2.0 + fun Int.migrateMusicMode() = when (this) { - // Convert PlaybackMode into MusicMode - IntegerTable.PLAYBACK_MODE_ALL_SONGS -> MusicMode.SONGS - IntegerTable.PLAYBACK_MODE_IN_ARTIST -> MusicMode.ARTISTS - IntegerTable.PLAYBACK_MODE_IN_ALBUM -> MusicMode.ALBUMS - IntegerTable.PLAYBACK_MODE_IN_GENRE -> MusicMode.GENRES + IntegerTable.MUSIC_MODE_SONGS -> PlaySong.FromAll + IntegerTable.MUSIC_MODE_ALBUMS -> PlaySong.FromAlbum + IntegerTable.MUSIC_MODE_ARTISTS -> PlaySong.FromArtist(null) + IntegerTable.MUSIC_MODE_GENRES -> PlaySong.FromGenre(null) else -> null } - if (sharedPreferences.contains(OLD_KEY_LIB_PLAYBACK_MODE)) { - logD("Migrating $OLD_KEY_LIB_PLAYBACK_MODE") + if (sharedPreferences.contains(OLD_KEY_LIB_MUSIC_PLAYBACK_MODE)) { + logD("Migrating $OLD_KEY_LIB_MUSIC_PLAYBACK_MODE") val mode = sharedPreferences - .getInt(OLD_KEY_LIB_PLAYBACK_MODE, IntegerTable.PLAYBACK_MODE_ALL_SONGS) - .migratePlaybackMode() - ?: MusicMode.SONGS + .getInt(OLD_KEY_LIB_MUSIC_PLAYBACK_MODE, Int.MIN_VALUE) + .migrateMusicMode() sharedPreferences.edit { - putInt(getString(R.string.set_key_in_list_playback_mode), mode.intCode) - remove(OLD_KEY_LIB_PLAYBACK_MODE) + putInt( + getString(R.string.set_key_play_in_list_with), mode?.intCode ?: Int.MIN_VALUE) + remove(OLD_KEY_LIB_MUSIC_PLAYBACK_MODE) apply() } } - if (sharedPreferences.contains(OLD_KEY_DETAIL_PLAYBACK_MODE)) { - logD("Migrating $OLD_KEY_DETAIL_PLAYBACK_MODE") + if (sharedPreferences.contains(OLD_KEY_DETAIL_MUSIC_PLAYBACK_MODE)) { + logD("Migrating $OLD_KEY_DETAIL_MUSIC_PLAYBACK_MODE") val mode = sharedPreferences - .getInt(OLD_KEY_DETAIL_PLAYBACK_MODE, Int.MIN_VALUE) - .migratePlaybackMode() + .getInt(OLD_KEY_DETAIL_MUSIC_PLAYBACK_MODE, Int.MIN_VALUE) + .migrateMusicMode() sharedPreferences.edit { putInt( - getString(R.string.set_key_in_parent_playback_mode), - mode?.intCode ?: Int.MIN_VALUE) - remove(OLD_KEY_DETAIL_PLAYBACK_MODE) + getString(R.string.set_key_play_in_parent_with), mode?.intCode ?: Int.MIN_VALUE) + remove(OLD_KEY_DETAIL_MUSIC_PLAYBACK_MODE) apply() } } @@ -216,8 +191,7 @@ class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Cont } private companion object { - const val OLD_KEY_ALT_NOTIF_ACTION = "KEY_ALT_NOTIF_ACTION" - const val OLD_KEY_LIB_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2" - const val OLD_KEY_DETAIL_PLAYBACK_MODE = "auxio_detail_song_play_mode" + const val OLD_KEY_LIB_MUSIC_PLAYBACK_MODE = "auxio_library_playback_mode" + const val OLD_KEY_DETAIL_MUSIC_PLAYBACK_MODE = "auxio_detail_playback_mode" } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index f329e2a70..e497c96ad 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -27,13 +27,12 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import org.oxycblt.auxio.list.ListSettings import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre -import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicRepository -import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.persist.PersistenceRepository @@ -59,8 +58,8 @@ constructor( private val playbackManager: PlaybackStateManager, private val playbackSettings: PlaybackSettings, private val persistenceRepository: PersistenceRepository, + private val listSettings: ListSettings, private val musicRepository: MusicRepository, - private val musicSettings: MusicSettings ) : ViewModel(), PlaybackStateManager.Listener, PlaybackSettings.Listener { private var lastPositionJob: Job? = null @@ -68,6 +67,7 @@ constructor( /** The currently playing song. */ val song: StateFlow get() = _song + private val _parent = MutableStateFlow(null) /** The [MusicParent] currently being played. Null if playback is occurring from all songs. */ val parent: StateFlow = _parent @@ -75,6 +75,7 @@ constructor( /** Whether playback is ongoing or paused. */ val isPlaying: StateFlow get() = _isPlaying + private val _positionDs = MutableStateFlow(0L) /** The current position, in deci-seconds (1/10th of a second). */ val positionDs: StateFlow @@ -84,6 +85,7 @@ constructor( /** The current [RepeatMode]. */ val repeatMode: StateFlow get() = _repeatMode + private val _isShuffled = MutableStateFlow(false) /** Whether the queue is shuffled or not. */ val isShuffled: StateFlow @@ -180,32 +182,23 @@ constructor( // --- 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. */ fun shuffleAll() { logD("Shuffling all songs") - playImpl(null, 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.") - } + playFromAllImpl(null, true) } /** @@ -216,16 +209,7 @@ constructor( * be prompted on what artist to play. Defaults to null. */ fun playFromArtist(song: Song, artist: Artist? = null) { - if (artist != null) { - 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)) - } + playFromArtistImpl(song, artist, isImplicitlyShuffled()) } /** @@ -236,19 +220,67 @@ constructor( * be prompted on what artist to play. Defaults to null. */ fun playFromGenre(song: Song, genre: Genre? = null) { - if (genre != null) { - logD("Playing $song from $genre") - playImpl(song, genre) - } else if (song.genres.size == 1) { - logD("$song has one genre, playing from it") - playImpl(song, song.genres[0]) - } else { - logD("$song has multiple genres, showing choice dialog") - startPlaybackDecisionImpl(PlaybackDecision.PlayFromGenre(song)) + playFromGenreImpl(song, genre, isImplicitlyShuffled()) + } + + private fun isImplicitlyShuffled() = + playbackManager.queue.isShuffled && playbackSettings.keepShuffle + + private fun playWithImpl(song: Song, with: PlaySong, shuffled: Boolean) { + when (with) { + 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 if (existing != null) { logD("Already handling decision $existing, ignoring $decision") @@ -257,17 +289,6 @@ constructor( _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]. * @@ -278,46 +299,6 @@ constructor( 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) { - logD("Playing ${songs.size} songs") - playbackManager.play(null, null, songs, false) - } - /** * Shuffle an [Album]. * @@ -328,6 +309,16 @@ constructor( 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]. * @@ -338,6 +329,16 @@ constructor( 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]. * @@ -348,6 +349,16 @@ constructor( 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]. * @@ -358,6 +369,16 @@ constructor( playImpl(null, playlist, true) } + /** + * Play a list of [Song]s. + * + * @param songs The [Song]s to play. + */ + fun play(songs: List) { + logD("Playing ${songs.size} songs") + playbackManager.play(null, null, songs, false) + } + /** * Shuffle a list of [Song]s. * @@ -368,22 +389,23 @@ constructor( playbackManager.play(null, null, songs, true) } - private fun playImpl( - song: Song?, - parent: MusicParent?, - shuffled: Boolean = playbackManager.queue.isShuffled && playbackSettings.keepShuffle - ) { + private fun playImpl(song: Song?, queue: List, shuffled: Boolean) { + check(song == null || queue.contains(song)) { "Song to play not in queue" } + playbackManager.play(song, null, queue, shuffled) + } + + private fun playImpl(song: Song?, parent: MusicParent?, shuffled: Boolean) { check(song == null || parent == null || parent.songs.contains(song)) { "Song to play not in parent" } val deviceLibrary = musicRepository.deviceLibrary ?: return val queue = when (parent) { - is Genre -> musicSettings.genreSongSort.songs(parent.songs) - is Artist -> musicSettings.artistSongSort.songs(parent.songs) - is Album -> musicSettings.albumSongSort.songs(parent.songs) + is Genre -> listSettings.genreSongSort.songs(parent.songs) + is Artist -> listSettings.artistSongSort.songs(parent.songs) + is Album -> listSettings.albumSongSort.songs(parent.songs) is Playlist -> parent.songs - null -> musicSettings.songSort.songs(deviceLibrary.songs) + null -> listSettings.songSort.songs(deviceLibrary.songs) } playbackManager.play(song, parent, queue, shuffled) } @@ -442,7 +464,7 @@ constructor( */ fun playNext(album: Album) { 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) { 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) { 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) { 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) { 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) { logD("Adding $genre to queue") - playbackManager.addToQueue(musicSettings.genreSongSort.songs(genre.songs)) + playbackManager.addToQueue(listSettings.genreSongSort.songs(genre.songs)) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt index c5ebf9904..bc421b846 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt @@ -114,12 +114,14 @@ class MutableQueue : Queue { @Volatile override var index = -1 private set + override val currentSong: Song? get() = shuffledMapping .ifEmpty { orderedMapping.ifEmpty { null } } ?.getOrNull(index) ?.let(heap::get) + override val isShuffled: Boolean get() = shuffledMapping.isNotEmpty() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt index 5c039ab8c..bd4ef874c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt @@ -22,7 +22,7 @@ import androidx.media3.common.C import androidx.media3.common.Format import androidx.media3.common.Player import androidx.media3.common.audio.AudioProcessor -import androidx.media3.exoplayer.audio.BaseAudioProcessor +import androidx.media3.common.audio.BaseAudioProcessor import java.nio.ByteBuffer import javax.inject.Inject import kotlin.math.pow @@ -81,6 +81,7 @@ constructor( applyReplayGain(queue.currentSong) } } + override fun onNewPlayback(queue: Queue, parent: MusicParent?) { logD("New playback started, updating playback information") applyReplayGain(queue.currentSong) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/InternalPlayer.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/InternalPlayer.kt index 17186e181..0980a1575 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/InternalPlayer.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/InternalPlayer.kt @@ -77,13 +77,13 @@ interface InternalPlayer { /** Possible long-running background tasks handled by the background playback task. */ sealed interface Action { /** 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" * shortcut. */ - object ShuffleAll : Action + data object ShuffleAll : Action /** * Start playing an audio file at the given [Uri]. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index ecbce22ee..7071f5111 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -310,15 +310,18 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Volatile override var parent: MusicParent? = null private set + @Volatile override var playerState = InternalPlayer.State.from(isPlaying = false, isAdvancing = false, 0) private set + @Volatile override var repeatMode = RepeatMode.NONE set(value) { field = value notifyRepeatModeChanged() } + override val currentAudioSessionId: Int? get() = internalPlayer?.audioSessionId diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.kt index b875636b8..65192cf10 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.kt @@ -37,6 +37,7 @@ import org.oxycblt.auxio.util.logD class MediaButtonReceiver : BroadcastReceiver() { @Inject lateinit var playbackManager: PlaybackStateManager + // TODO: Figure this out override fun onReceive(context: Context, intent: Intent) { if (playbackManager.queue.currentSong != null) { // We have a song, so we can assume that the service will start a foreground state. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt index b6e0819d0..8e6ff346f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt @@ -339,7 +339,7 @@ constructor( object : BitmapProvider.Target { override fun onCompleted(bitmap: Bitmap?) { 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_ALBUM_ART, bitmap) val metadata = builder.build() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index ffcc84b41..c131238e0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -25,8 +25,8 @@ import android.content.Intent import android.content.IntentFilter import android.media.AudioManager import android.media.audiofx.AudioEffect -import android.os.Build import android.os.IBinder +import androidx.core.content.ContextCompat import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.MediaItem @@ -47,8 +47,8 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.oxycblt.auxio.BuildConfig +import org.oxycblt.auxio.list.ListSettings import org.oxycblt.auxio.music.MusicRepository -import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.playback.persist.PersistenceRepository @@ -99,8 +99,8 @@ class PlaybackService : @Inject lateinit var playbackManager: PlaybackStateManager @Inject lateinit var playbackSettings: PlaybackSettings @Inject lateinit var persistenceRepository: PersistenceRepository + @Inject lateinit var listSettings: ListSettings @Inject lateinit var musicRepository: MusicRepository - @Inject lateinit var musicSettings: MusicSettings // State private lateinit var foregroundManager: ForegroundManager @@ -165,18 +165,8 @@ class PlaybackService : addAction(WidgetProvider.ACTION_WIDGET_UPDATE) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - registerReceiver( - systemReceiver, - intentFilter, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - RECEIVER_NOT_EXPORTED - } else { - 0 - }) - } else { - registerReceiver(systemReceiver, intentFilter) - } + ContextCompat.registerReceiver( + this, systemReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED) logD("Service created") } @@ -379,7 +369,7 @@ class PlaybackService : is InternalPlayer.Action.ShuffleAll -> { logD("Shuffling all tracks") 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 is InternalPlayer.Action.Open -> { @@ -388,7 +378,7 @@ class PlaybackService : playbackManager.play( song, null, - musicSettings.songSort.songs(deviceLibrary.songs), + listSettings.songSort.songs(deviceLibrary.songs), playbackManager.queue.isShuffled && playbackSettings.keepShuffle) } } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index 9417f8a03..128a3c394 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -41,7 +41,7 @@ import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment 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.Artist import org.oxycblt.auxio.music.Genre @@ -174,7 +174,7 @@ class SearchFragment : ListFragment() { override fun onRealClick(item: Music) { when (item) { - is Song -> playbackModel.playFrom(item, searchModel.playbackMode) + is Song -> playbackModel.play(item, searchModel.playWith) is Album -> detailModel.showAlbum(item) is Artist -> detailModel.showArtist(item) is Genre -> detailModel.showGenre(item) @@ -182,9 +182,9 @@ class SearchFragment : ListFragment() { } } - override fun onOpenMenu(item: Music, anchor: View) { + override fun onOpenMenu(item: Music) { 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 Artist -> listModel.openMenu(R.menu.item_parent, item) is Genre -> listModel.openMenu(R.menu.item_parent, item) @@ -256,16 +256,11 @@ class SearchFragment : ListFragment() { if (menu == null) return val directions = when (menu) { - is Menu.ForSong -> - SearchFragmentDirections.openSongMenu(menu.menuRes, menu.music.uid) - is Menu.ForAlbum -> - SearchFragmentDirections.openAlbumMenu(menu.menuRes, menu.music.uid) - is Menu.ForArtist -> - 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) + is Menu.ForSong -> SearchFragmentDirections.openSongMenu(menu.parcel) + is Menu.ForAlbum -> SearchFragmentDirections.openAlbumMenu(menu.parcel) + is Menu.ForArtist -> SearchFragmentDirections.openArtistMenu(menu.parcel) + is Menu.ForGenre -> SearchFragmentDirections.openGenreMenu(menu.parcel) + is Menu.ForPlaylist -> SearchFragmentDirections.openPlaylistMenu(menu.parcel) } findNavController().navigateSafe(directions) // Keyboard is no longer needed. diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchModule.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchModule.kt index 18c857ba4..fb8e7e063 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchModule.kt @@ -27,5 +27,6 @@ import dagger.hilt.components.SingletonComponent @InstallIn(SingletonComponent::class) interface SearchModule { @Binds fun engine(searchEngine: SearchEngineImpl): SearchEngine + @Binds fun settings(searchSettings: SearchSettingsImpl): SearchSettings } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchSettings.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchSettings.kt index 16edab48b..71fd94583 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchSettings.kt @@ -23,7 +23,7 @@ import androidx.core.content.edit import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import org.oxycblt.auxio.R -import org.oxycblt.auxio.music.MusicMode +import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.settings.Settings /** @@ -32,19 +32,21 @@ import org.oxycblt.auxio.settings.Settings * @author Alexander Capehart (OxygenCobalt) */ interface SearchSettings : Settings { - /** The type of Music the search view is currently filtering to. */ - var searchFilterMode: MusicMode? + /** The type of Music the search view is should filter to. */ + var filterTo: MusicType? } class SearchSettingsImpl @Inject constructor(@ApplicationContext context: Context) : Settings.Impl(context), SearchSettings { - override var searchFilterMode: MusicMode? + override var filterTo: MusicType? get() = - MusicMode.fromIntCode( - sharedPreferences.getInt(getString(R.string.set_key_search_filter), Int.MIN_VALUE)) + MusicType.fromIntCode( + sharedPreferences.getInt( + getString(R.string.set_key_search_filter_to), Int.MIN_VALUE)) set(value) { 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() } } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index 8a3aa5a1c..fb60d7ff9 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -32,12 +32,13 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.list.BasicHeader import org.oxycblt.auxio.list.Divider import org.oxycblt.auxio.list.Item -import org.oxycblt.auxio.list.Sort -import org.oxycblt.auxio.music.MusicMode +import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.music.MusicRepository +import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.user.UserLibrary +import org.oxycblt.auxio.playback.PlaySong import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.util.logD @@ -63,9 +64,9 @@ constructor( val searchResults: StateFlow> get() = _searchResults - /** The [MusicMode] to use when playing a [Song] from the UI. */ - val playbackMode: MusicMode - get() = playbackSettings.inListPlaybackMode + /** The [PlaySong] instructions to use when playing a [Song]. */ + val playWith + get() = playbackSettings.playInListWith init { musicRepository.addUpdateListener(this) @@ -116,12 +117,12 @@ constructor( userLibrary: UserLibrary, query: String ): List { - val filterMode = searchSettings.searchFilterMode + val filter = searchSettings.filterTo val items = - if (filterMode == null) { - // A nulled filter mode means to not filter anything. - logD("No filter mode specified, using entire library") + if (filter == null) { + // A nulled filter type means to not filter anything. + logD("No filter specified, using entire library") SearchEngine.Items( deviceLibrary.songs, deviceLibrary.albums, @@ -129,14 +130,13 @@ constructor( deviceLibrary.genres, userLibrary.playlists) } else { - logD("Filter mode specified, filtering library") + logD("Filter specified, reducing library") SearchEngine.Items( - songs = if (filterMode == MusicMode.SONGS) deviceLibrary.songs else null, - albums = if (filterMode == MusicMode.ALBUMS) deviceLibrary.albums else null, - artists = if (filterMode == MusicMode.ARTISTS) deviceLibrary.artists else null, - genres = if (filterMode == MusicMode.GENRES) deviceLibrary.genres else null, - playlists = - if (filterMode == MusicMode.PLAYLISTS) userLibrary.playlists else null) + songs = if (filter == MusicType.SONGS) deviceLibrary.songs else null, + albums = if (filter == MusicType.ALBUMS) deviceLibrary.albums else null, + artists = if (filter == MusicType.ARTISTS) deviceLibrary.artists else null, + genres = if (filter == MusicType.GENRES) deviceLibrary.genres else null, + playlists = if (filter == MusicType.PLAYLISTS) userLibrary.playlists else null) } val results = searchEngine.search(items, query) @@ -198,35 +198,35 @@ constructor( */ @IdRes fun getFilterOptionId() = - when (searchSettings.searchFilterMode) { - MusicMode.SONGS -> R.id.option_filter_songs - MusicMode.ALBUMS -> R.id.option_filter_albums - MusicMode.ARTISTS -> R.id.option_filter_artists - MusicMode.GENRES -> R.id.option_filter_genres - MusicMode.PLAYLISTS -> R.id.option_filter_playlists + when (searchSettings.filterTo) { + MusicType.SONGS -> R.id.option_filter_songs + MusicType.ALBUMS -> R.id.option_filter_albums + MusicType.ARTISTS -> R.id.option_filter_artists + MusicType.GENRES -> R.id.option_filter_genres + MusicType.PLAYLISTS -> R.id.option_filter_playlists // Null maps to filtering nothing. 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. */ fun setFilterOptionId(@IdRes id: Int) { - val newFilterMode = + val newFilter = when (id) { - R.id.option_filter_songs -> MusicMode.SONGS - R.id.option_filter_albums -> MusicMode.ALBUMS - R.id.option_filter_artists -> MusicMode.ARTISTS - R.id.option_filter_genres -> MusicMode.GENRES - R.id.option_filter_playlists -> MusicMode.PLAYLISTS + R.id.option_filter_songs -> MusicType.SONGS + R.id.option_filter_albums -> MusicType.ALBUMS + R.id.option_filter_artists -> MusicType.ARTISTS + R.id.option_filter_genres -> MusicType.GENRES + R.id.option_filter_playlists -> MusicType.PLAYLISTS // Null maps to filtering nothing. R.id.option_filter_all -> null else -> error("Invalid option ID provided") } - logD("Updating filter mode to $newFilterMode") - searchSettings.searchFilterMode = newFilterMode + logD("Updating filter type to $newFilter") + searchSettings.filterTo = newFilter search(lastQuery) } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt index 8288d7443..cd6217a9f 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt @@ -121,7 +121,6 @@ class AboutFragment : ViewBindingFragment() { // case, we will try to manually handle these cases before we try to launch the // browser. logD("Resolving browser activity for chooser") - @Suppress("DEPRECATION") val pkgName = context.packageManager .resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY) diff --git a/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreferenceDialog.kt b/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreferenceDialog.kt index 2995b6353..67ad6b1f3 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreferenceDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreferenceDialog.kt @@ -35,6 +35,7 @@ import org.oxycblt.auxio.util.fixDoubleRipple class IntListPreferenceDialog : PreferenceDialogFragmentCompat() { private val listPreference: IntListPreference get() = (preference as IntListPreference) + private var pendingValueIndex = -1 override fun onStart() { diff --git a/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt index 9937d1eac..0299a6af8 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt @@ -76,6 +76,7 @@ abstract class BaseBottomSheetBehavior(context: Context, attributeSet: // Enable experimental settings that allow us to skip the half-expanded state. override fun shouldSkipHalfExpandedStateWhenDragging() = true + override fun shouldExpandOnUpwardDrag(dragDurationMillis: Long, yPositionPercentage: Float) = true diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt index 2072d62e7..3a5adce95 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt @@ -18,13 +18,16 @@ package org.oxycblt.auxio.ui +import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AlertDialog +import androidx.annotation.StyleRes import androidx.fragment.app.DialogFragment 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 org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull @@ -39,13 +42,8 @@ abstract class ViewBindingBottomSheetDialogFragment : BottomSheetDialogFragment() { private var _binding: VB? = null - /** - * Configure the [AlertDialog.Builder] during [onCreateDialog]. - * - * @param builder The [AlertDialog.Builder] to configure. - * @see onCreateDialog - */ - protected open fun onConfigDialog(builder: AlertDialog.Builder) {} + override fun onCreateDialog(savedInstanceState: Bundle?): BottomSheetDialog = + TweakedBottomSheetDialog(requireContext(), theme) /** * Inflate the [ViewBinding] during [onCreateView]. @@ -108,4 +106,22 @@ abstract class ViewBindingBottomSheetDialogFragment : _binding = null 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 + } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt index de03d3ba9..60ca375c0 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt @@ -22,7 +22,7 @@ import android.os.Build import org.oxycblt.auxio.R import org.oxycblt.auxio.util.logW -private val ACCENT_NAMES = +private val accentNames = intArrayOf( R.string.clr_red, R.string.clr_pink, @@ -42,7 +42,7 @@ private val ACCENT_NAMES = R.string.clr_grey, R.string.clr_dynamic) -private val ACCENT_THEMES = +private val accentThemes = intArrayOf( R.style.Theme_Auxio_Red, 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 ) -private val ACCENT_BLACK_THEMES = +private val accentBlackThemes = intArrayOf( R.style.Theme_Auxio_Black_Red, 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 ) -private val ACCENT_PRIMARY_COLORS = +private val accentPrimaryColors = intArrayOf( R.color.red_primary, R.color.pink_primary, @@ -115,18 +115,18 @@ private val ACCENT_PRIMARY_COLORS = class Accent private constructor(val index: Int) { /** The name of this [Accent]. */ val name: Int - get() = ACCENT_NAMES[index] + get() = accentNames[index] /** The theme resource for this accent. */ 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. */ val blackTheme: Int - get() = ACCENT_BLACK_THEMES[index] + get() = accentBlackThemes[index] /** The accent's primary color. */ val primary: Int - get() = ACCENT_PRIMARY_COLORS[index] + get() = accentPrimaryColors[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 = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Use dynamic coloring on devices that support it. - ACCENT_THEMES.lastIndex + accentThemes.lastIndex } else { // Use blue everywhere else. 5 @@ -161,10 +161,10 @@ class Accent private constructor(val index: Int) { /** The amount of valid accents. */ val MAX = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - ACCENT_THEMES.size + accentThemes.size } else { // Disable the option for a dynamic accent on unsupported devices. - ACCENT_THEMES.size - 1 + accentThemes.size - 1 } } } diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index d06c0ca37..e3f50ccec 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -25,16 +25,20 @@ import android.os.Build import android.view.View import android.view.WindowInsets import androidx.annotation.RequiresApi +import androidx.appcompat.view.menu.ActionMenuItemView +import androidx.appcompat.widget.ActionMenuView import androidx.appcompat.widget.AppCompatButton import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.app.ShareCompat import androidx.core.graphics.Insets import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.children import androidx.navigation.NavController import androidx.navigation.NavDirections import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding +import com.google.android.material.appbar.MaterialToolbar import java.lang.IllegalArgumentException import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song @@ -103,6 +107,25 @@ val Drawable.isRtl: Boolean val ViewBinding.context: Context get() = root.context +/** + * Override the behavior of a [MaterialToolbar]'s overflow menu to do something else. This is + * extremely dumb, but required to hook overflow menus to bottom sheet menus. + */ +fun MaterialToolbar.overrideOnOverflowMenuClick(block: (View) -> Unit) { + for (toolbarChild in children) { + if (toolbarChild is ActionMenuView) { + for (menuChild in toolbarChild.children) { + // The overflow menu's view implementation is package-private, so test for the + // first child that isn't a plain action button. + if (menuChild !is ActionMenuItemView) { + menuChild.setOnClickListener(block) + return + } + } + } + } +} + /** * Compute if this [RecyclerView] can scroll through their items, or if the items can all fit on one * screen. diff --git a/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt index c1e1a4a92..eb74d8f15 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt @@ -52,6 +52,7 @@ interface Event { */ class MutableEvent : Event { override val flow = MutableStateFlow(null) + override fun consume() = flow.value?.also { flow.value = null } /** diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt index bb2e7b54f..e6f86736a 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -137,13 +137,18 @@ constructor( // Respond to all major song or player changes that will affect the widget override fun onIndexMoved(queue: Queue) = update() + override fun onQueueReordered(queue: Queue) = update() + override fun onNewPlayback(queue: Queue, parent: MusicParent?) = update() + override fun onStateChanged(state: InternalPlayer.State) = update() + override fun onRepeatChanged(repeatMode: RepeatMode) = update() // Respond to settings changes that will affect the widget override fun onRoundModeChanged() = update() + override fun onImageSettingsChanged() = update() /** diff --git a/app/src/main/res/anim/bottom_sheet_slide_in.xml b/app/src/main/res/anim/bottom_sheet_slide_in.xml new file mode 100644 index 000000000..e9236ec2d --- /dev/null +++ b/app/src/main/res/anim/bottom_sheet_slide_in.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/anim/bottom_sheet_slide_out.xml b/app/src/main/res/anim/bottom_sheet_slide_out.xml new file mode 100644 index 000000000..3337807ab --- /dev/null +++ b/app/src/main/res/anim/bottom_sheet_slide_out.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/layout-w600dp-land/fragment_main.xml b/app/src/main/res/layout-w600dp-land/fragment_main.xml index e7213924f..e381eae0f 100644 --- a/app/src/main/res/layout-w600dp-land/fragment_main.xml +++ b/app/src/main/res/layout-w600dp-land/fragment_main.xml @@ -13,7 +13,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior" - app:navGraph="@navigation/main" + app:navGraph="@navigation/inner" app:defaultNavHost="true" tools:layout="@layout/fragment_home" /> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 6064ea0e4..23d23770a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -3,8 +3,10 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_host" - android:name="org.oxycblt.auxio.MainFragment" + android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" android:background="?attr/colorSurface" + app:defaultNavHost="true" + app:navGraph="@navigation/outer" tools:layout="@layout/fragment_main" /> \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_menu.xml b/app/src/main/res/layout/dialog_menu.xml index c685bfc9a..1a5a7a605 100644 --- a/app/src/main/res/layout/dialog_menu.xml +++ b/app/src/main/res/layout/dialog_menu.xml @@ -1,72 +1,90 @@ - - + - - - - - - - + android:layout_height="wrap_content"> + + + + + + + + + + + + + + - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_music_dirs.xml b/app/src/main/res/layout/dialog_music_dirs.xml index 49458926a..fa48b8a9c 100644 --- a/app/src/main/res/layout/dialog_music_dirs.xml +++ b/app/src/main/res/layout/dialog_music_dirs.xml @@ -27,7 +27,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="@dimen/spacing_large" - android:layout_marginTop="@dimen/spacing_small" + android:layout_marginTop="@dimen/spacing_tiny" android:layout_marginEnd="@dimen/spacing_large" android:gravity="center" app:layout_constraintTop_toBottomOf="@+id/dirs_mode_header" diff --git a/app/src/main/res/layout/dialog_sort.xml b/app/src/main/res/layout/dialog_sort.xml new file mode 100644 index 000000000..bf6ba7511 --- /dev/null +++ b/app/src/main/res/layout/dialog_sort.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_detail.xml b/app/src/main/res/layout/fragment_detail.xml index 1dfcf82b9..d3a9409da 100644 --- a/app/src/main/res/layout/fragment_detail.xml +++ b/app/src/main/res/layout/fragment_detail.xml @@ -24,6 +24,7 @@ android:layout_height="wrap_content" android:clickable="true" android:focusable="true" + app:menu="@menu/toolbar_detail" app:navigationIcon="@drawable/ic_back_24" /> diff --git a/app/src/main/res/layout/fragment_preferences.xml b/app/src/main/res/layout/fragment_preferences.xml index e4fc1dd98..a66497d9d 100644 --- a/app/src/main/res/layout/fragment_preferences.xml +++ b/app/src/main/res/layout/fragment_preferences.xml @@ -1,7 +1,6 @@ + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" /> diff --git a/app/src/main/res/layout/item_edit_header.xml b/app/src/main/res/layout/item_edit_header.xml index 80659deca..e3bbb9009 100644 --- a/app/src/main/res/layout/item_edit_header.xml +++ b/app/src/main/res/layout/item_edit_header.xml @@ -28,14 +28,14 @@ app:icon="@drawable/ic_edit_24" app:layout_constraintEnd_toEndOf="parent" /> - - - - - - - - - + \ No newline at end of file diff --git a/app/src/main/res/layout/item_sort_mode.xml b/app/src/main/res/layout/item_sort_mode.xml new file mode 100644 index 000000000..7d8129737 --- /dev/null +++ b/app/src/main/res/layout/item_sort_mode.xml @@ -0,0 +1,28 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/item_album_song.xml b/app/src/main/res/menu/item_album_song.xml index e31ea20b7..fdbc3fc5f 100644 --- a/app/src/main/res/menu/item_album_song.xml +++ b/app/src/main/res/menu/item_album_song.xml @@ -1,13 +1,13 @@ - - - - - - - - + + - + - - - - - - - - + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/item_detail_parent.xml b/app/src/main/res/menu/item_detail_parent.xml new file mode 100644 index 000000000..a6a1b6d09 --- /dev/null +++ b/app/src/main/res/menu/item_detail_parent.xml @@ -0,0 +1,27 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/item_detail_playlist.xml b/app/src/main/res/menu/item_detail_playlist.xml new file mode 100644 index 000000000..178588c3b --- /dev/null +++ b/app/src/main/res/menu/item_detail_playlist.xml @@ -0,0 +1,31 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/item_playback_song.xml b/app/src/main/res/menu/item_playback_song.xml new file mode 100644 index 000000000..2cfc524b2 --- /dev/null +++ b/app/src/main/res/menu/item_playback_song.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/item_playlist_song.xml b/app/src/main/res/menu/item_playlist_song.xml index 2063869d2..934d8b514 100644 --- a/app/src/main/res/menu/item_playlist_song.xml +++ b/app/src/main/res/menu/item_playlist_song.xml @@ -1,13 +1,13 @@ - - - - - - - - + + + + - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/sort_artist.xml b/app/src/main/res/menu/sort_artist.xml deleted file mode 100644 index 50ba847ba..000000000 --- a/app/src/main/res/menu/sort_artist.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/sort_genre.xml b/app/src/main/res/menu/sort_genre.xml deleted file mode 100644 index ad5d920c8..000000000 --- a/app/src/main/res/menu/sort_genre.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/toolbar_album.xml b/app/src/main/res/menu/toolbar_album.xml deleted file mode 100644 index 870350cc4..000000000 --- a/app/src/main/res/menu/toolbar_album.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/toolbar_detail.xml b/app/src/main/res/menu/toolbar_detail.xml new file mode 100644 index 000000000..a827b708e --- /dev/null +++ b/app/src/main/res/menu/toolbar_detail.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/toolbar_home.xml b/app/src/main/res/menu/toolbar_home.xml index 278206a03..9aa0360de 100644 --- a/app/src/main/res/menu/toolbar_home.xml +++ b/app/src/main/res/menu/toolbar_home.xml @@ -9,46 +9,10 @@ app:showAsAction="ifRoom" /> - - - - - - - - - - - - - - - - + app:showAsAction="ifRoom" /> - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/toolbar_playback.xml b/app/src/main/res/menu/toolbar_playback.xml index e283a6bc1..27791e20a 100644 --- a/app/src/main/res/menu/toolbar_playback.xml +++ b/app/src/main/res/menu/toolbar_playback.xml @@ -5,23 +5,9 @@ android:id="@+id/action_open_equalizer" android:icon="@drawable/ic_config_24" android:title="@string/lbl_equalizer" - app:showAsAction="ifRoom" /> + app:showAsAction="always" /> - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/toolbar_playlist.xml b/app/src/main/res/menu/toolbar_playlist.xml deleted file mode 100644 index 666629234..000000000 --- a/app/src/main/res/menu/toolbar_playlist.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/main.xml b/app/src/main/res/navigation/inner.xml similarity index 76% rename from app/src/main/res/navigation/main.xml rename to app/src/main/res/navigation/inner.xml index 1d6c1a8ee..979b09b2d 100644 --- a/app/src/main/res/navigation/main.xml +++ b/app/src/main/res/navigation/inner.xml @@ -13,11 +13,20 @@ android:id="@+id/search" app:destination="@id/search_fragment" /> + android:id="@+id/sort_songs" + app:destination="@+id/song_sort_dialog" /> + android:id="@+id/sort_albums" + app:destination="@+id/album_sort_dialog" /> + + + @@ -71,6 +80,36 @@ app:destination="@id/play_from_genre_dialog" /> + + + + + + + + + + + @@ -159,6 +201,9 @@ + @@ -170,6 +215,12 @@ app:destination="@id/play_from_genre_dialog" /> + + + @@ -193,6 +247,9 @@ + @@ -201,6 +258,12 @@ app:destination="@id/play_from_genre_dialog" /> + + + @@ -227,6 +293,9 @@ + @@ -235,6 +304,12 @@ app:destination="@id/play_from_artist_dialog" /> + + + @@ -258,6 +336,9 @@ + @@ -272,6 +353,12 @@ app:destination="@id/play_from_genre_dialog" /> + + - + android:name="parcel" + app:argType="org.oxycblt.auxio.list.menu.Menu$ForSong$Parcel" /> - + android:name="parcel" + app:argType="org.oxycblt.auxio.list.menu.Menu$ForAlbum$Parcel" /> - + android:name="parcel" + app:argType="org.oxycblt.auxio.list.menu.Menu$ForArtist$Parcel" /> - + android:name="parcel" + app:argType="org.oxycblt.auxio.list.menu.Menu$ForGenre$Parcel" /> - + android:name="parcel" + app:argType="org.oxycblt.auxio.list.menu.Menu$ForPlaylist$Parcel" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/outer.xml b/app/src/main/res/navigation/outer.xml new file mode 100644 index 000000000..23491b998 --- /dev/null +++ b/app/src/main/res/navigation/outer.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ar-rIQ/strings.xml b/app/src/main/res/values-ar-rIQ/strings.xml index fbb6ab074..9dc9abb35 100644 --- a/app/src/main/res/values-ar-rIQ/strings.xml +++ b/app/src/main/res/values-ar-rIQ/strings.xml @@ -1,7 +1,7 @@ - مشغل موسيقى بسيط ومعقول لنظام الاندرويد. + مشغل موسيقى بسيط ومعقول للأندرويد عرض وتحكم بشتغيل الموسيقى إعادة المحاولة @@ -23,9 +23,9 @@ يتم التشغيل الان تشغيل عشوائي - تشغيل من جميع الاغاني - تشغيل من البوم - تشغيل من فنان + تشغيل من جميع الاغاني + تشغيل من البوم + تشغيل من فنان طابور شغل الاغنية التالية أضف إلى الطابور @@ -63,7 +63,7 @@ تفضيل الالبوم ديناميكي سلوك - عند اختيار اغنية + عند اختيار اغنية تذكر الخلط إبقاء وضع الخلط عند تشغيل اغنية جديدة تشجيع قبل التخطي للخلف @@ -157,7 +157,7 @@ تشغيل كل الاغاني بشكل عشوائي حسنا اعادة الحالة - تنازلي + تنازلي عرض الخصائص مسح الحالة مباشر diff --git a/app/src/main/res/values-ar-rSA/strings.xml b/app/src/main/res/values-ar-rSA/strings.xml index a6b3daec9..3a0906840 100644 --- a/app/src/main/res/values-ar-rSA/strings.xml +++ b/app/src/main/res/values-ar-rSA/strings.xml @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index a6b3daec9..fcbf1ee96 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1,2 +1,85 @@ - \ No newline at end of file + + مشغّل موسيقى بسيط ومعقول للأندرويد + مراقبة مكتبة الموسيقى + إعادة المحاولة + منح + الألبومات + أغاني + أغنية + كل الأغاني + ألبوم + ألبوم مباشر + تجميعات + حذف قائمة التشغيل؟ + بحث + تصفية + تشغيل المختارة + تشغيل التالي + إضافة للطابور + إضافة لقائمة التشغيل + إعادة ضبط + إضافة مجلد + تم حفظ الحالة + يتم تحميل مكتبتك الموسيقية + أضيفت للطابور + تم إنشاء قائمة التشغيل + فنانون + قائمة تشغيل جديدة + إعادة تسمية قائمة التشغيل + تعديل + خلط المختارة + طابور + خلط + اذهب للفنان + عرض والتحكم في تشغيل الموسيقى + خلط + اسم الملف + خلط الكل + إلغاء + حفظ + تتم مراقبة التغييرات في مكتبك الموسيقية… + تجميعة + تجميعة مباشرة + مباشر + ظهر فيه + فنان + ريميكسات + نوع + أنواع + قائمة تشغيل + قوائم تشغيل + إعادة تسمية + حذف + الكل + الاسم + التاريخ + المدة + عدد الأغاني + القرص + المسار + تاريخ الإضافة + فرز + تصاعدياً + تنازلياً + يتم الآن تشغيل + المُعادِل + تشغيل + اذهب للألبوم + عرض الخصائص + عرض + مشاركة + خصائص الأغنية + التنسيق + الحجم + معدل البِت + موافق + تم حذف الحالة + تمت استعادة الحالة + حول + الإصدار + شفرة المصدر + الموسوعة + التراخيص + إحصائيات المكتبة + \ No newline at end of file diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 8ff87a97e..1b783cd27 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -12,7 +12,7 @@ Дададзены ў чаргу Распрацавана Аляксандрам Кейпхартам Карыстальніцкае дзеянне панэлі прайгравання - Прайграць з альбома + Прайграць з альбома Коска (,) Плюс (+) Амперсанд (&) @@ -77,7 +77,7 @@ Ператасаваць Адмяніць Па ўзрастанні - Па змяншэнні + Па змяншэнні Гуляць далей Дадаць у чаргу Эквалайзер @@ -234,15 +234,15 @@ Перайсці да наступнага Рэжым паўтору Паводзіны - Пры прайграванні з бібліятэкі - Прайграць усе песні + Пры прайграванні з бібліятэкі + Прайграць усе песні Кіруйце загрузкай музыкі і малюнкаў Карыстальніцкае дзеянне апавяшчэння - Пры прайграванні з дэталяў прадмета - Гуляць з паказанага прадмета + Пры прайграванні з дэталяў прадмета + Гуляць з паказанага прадмета Ігнаруйце аўдыяфайлы, якія не з\'яўляюцца музыкай, напрыклад, падкасты - Гуляць ад выканаўцы - Гуляць з жанру + Гуляць ад выканаўцы + Гуляць з жанру Запамінаць перамешванне Уключайце перамешванне падчас прайгравання новай песні Кантэнт @@ -296,4 +296,6 @@ Выкарыстоўваць квадратныя вокладкі альбомаў Абрэзаць усе вокладкі альбомаў да суадносін бакоў 1:1 Песня + Прайграць песню самастойна + Выгляд \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 68c3f3e4f..d53bff062 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -29,9 +29,9 @@ Právě hraje Přehrát Náhodně - Přehrát ze všech skladeb - Přehrát z alba - Přehrát od umělce + Přehrát ze všech skladeb + Přehrát z alba + Přehrát od umělce Fronta Přehrát další Přidat do fronty @@ -80,7 +80,7 @@ Přizpůsobení bez štítků Varování: Změna předzesilovače na vysokou kladnou hodnotu může u některých zvukových stop vést k příliš vysokým hlasitostem. Chování - Při přehrávání z knihovny + Při přehrávání z knihovny Zapamatovat si náhodné přehrávání Ponechat náhodné přehrávání při přehrávání nové skladby Přetočit před přeskočením zpět @@ -180,9 +180,9 @@ Free Lossless Audio Codec (FLAC) %d kbps %d Hz - Při přehrávání z podrobností o položce + Při přehrávání z podrobností o položce Spravovat, odkud by měla být načítána hudba - Přehrát ze zobrazené položky + Přehrát ze zobrazené položky Zobrazit vlastnosti Vlastnosti skladby Název souboru @@ -263,7 +263,7 @@ Přehrát vybrané Vybráno %d Náhodně přehrát vybrané - Přehrát z žánru + Přehrát z žánru Wiki %1$s, %2$s Obnovit @@ -279,7 +279,7 @@ Přehrávání Knihovna Perzistence - Sestupně + Sestupně Seznamy skladeb Obrázek seznamu skladeb pro %s Seznam skladeb @@ -308,4 +308,5 @@ Oříznout všechny covery alb na poměr stran 1:1 Skladba Zobrazit + Přehrát skladbu samostatně \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 0321ffeec..09d153413 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -15,8 +15,8 @@ Aufsteigend Abspielen Zufällig - Von allen Lieder abspielen - Von Album abspielen + Von allen Lieder abspielen + Von Album abspielen Aktuelle Wiedergabe Warteschlange Als Nächstes abspielen @@ -56,7 +56,7 @@ Titel bevorzugen Album bevorzugen Personalisieren - Wenn ein Lied aus der Bibliothek abgespielt wird + Wenn ein Lied aus der Bibliothek abgespielt wird Zufällig-Einstellung merken Zufällig anlassen, wenn ein neues Lied abgespielt wird Zurückspulen, bevor das Lied zurück geändert wird @@ -153,8 +153,8 @@ Gesamtdauer: %s Abbrechen Warnung: Das Erhöhen der Vorverstärkung zu einem hohen positiven Wert könnte zu einer Übersteuerung bei einigen Audiospuren führen. - Wenn ein Lied aus den Elementdetails abgespielt wird - Vom dargestellten Element abspielen + Wenn ein Lied aus den Elementdetails abgespielt wird + Vom dargestellten Element abspielen Musikordner Verwalten, von wo die Musik geladen werden soll Modus @@ -233,7 +233,7 @@ Komma (,) Schrägstrich (/) Plus (+) - Vom Künstler abspielen + Vom Künstler abspielen Achtung: Verwenden dieser Einstellung könnte dazu führen, dass einige Tags fälschlicherweise interpretiert werden, als hätten sie mehrere Werte. Das kann gelöst werden, in dem vor ungewollte Trenner ein Backslash (\\) eingefügt wird. Nicht-Musik ausschließen Audio-Dateien, die keine Musik sind (wie Podcasts), ignorieren @@ -254,7 +254,7 @@ Ausgewählte abspielen Ausgewählte zufällig abspielen %d ausgewählt - Vom Genre abspielen + Vom Genre abspielen Wiki %1$s, %2$s Zurücksetzen @@ -270,7 +270,7 @@ Ton und Wiedergabeverhalten konfigurieren Persistenz Lautstärkeanpassung ReplayGain - Absteigend + Absteigend Wiedergabelistenbild für %s Wiedergabeliste Wiedergabelisten diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d1a76b43b..c2e376431 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -23,9 +23,9 @@ En reproducción Reproducir Mezcla - Reproducir todo - Reproducir por álbum - Reproducir por artista + Reproducir todo + Reproducir por álbum + Reproducir por artista Cola Reproducir siguiente Agregar a la cola @@ -63,7 +63,7 @@ Por álbum Preferir el álbum si se está en reproducción Comportamiento - Cuando se está reproduciendo de la biblioteca + Cuando se está reproduciendo de la biblioteca Recordar mezcla Mantener mezcla cuando se reproduce una nueva canción Rebobinar atrás @@ -149,7 +149,7 @@ Estadísticas de la biblioteca Ajuste sin etiquetas Advertencia: Cambiar el pre-amp a un valor alto puede resultar en picos en algunas pistas de audio. - Reproducir desde el elemento que se muestra + Reproducir desde el elemento que se muestra Modo Excluir La músicano se cargará de las carpetas que añadas. @@ -171,7 +171,7 @@ Monitorizando la librería de música Monitorizando cambios en tu librería de música… Audio ogg - Cuando se reproduce desde los detalles + Cuando se reproduce desde los detalles Fecha de añadido Propiedades de la canción Frecuencia de muestreo @@ -258,7 +258,7 @@ Nodo aleatorio seleccionado %d seleccionado Reproducir los seleccionados - Reproducir desde el género + Reproducir desde el género Wiki %1$s, %2$s Restablecer @@ -274,7 +274,7 @@ Cambiar el tema y los colores de la aplicación Personalizar los controles y el comportamiento de la interfaz de usuario Biblioteca - Descendente + Descendente Listas de reproducción Imagen de la lista de reproducción para %s Lista de reproducción @@ -303,4 +303,5 @@ Recorta todas las portadas de los álbumes a una relación de aspecto 1:1 Canción Vista + Reproducir la canción por tí mismo \ No newline at end of file diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 21f8693dd..1f222e0a7 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -33,7 +33,7 @@ Levy Raita Lisäyspäivä - Laskevasti + Laskevasti Nyt toistetaan Taajuuskorjain Toista @@ -68,12 +68,12 @@ Muuta kirjastovälilehtien näkyvyyttä ja järjestystä Siirry seuraavaan Kertaustila - Kirjastosta toistettaessa - Kohteen tiedoista toistettaessa + Kirjastosta toistettaessa + Kohteen tiedoista toistettaessa Muista sekoitus - Toista kaikista kappaleista - Toista albumilta - Toista tyylilajista + Toista kaikista kappaleista + Toista albumilta + Toista tyylilajista Moniarvoerottimet Ohita äänitiedostot, jotka eivät ole musiikkia, kuten podcastit Ja-merkki (&) @@ -238,7 +238,7 @@ Tuntematon tyylilaji Vihreä Musiikkia ei toisteta - Toista esittäjältä + Toista esittäjältä Ohita muu kuin musiikki Palauta aiemmin tallennettu toiston tila (jos olemassa) Musiikkia ei ladata valitsemistasi kansioista. @@ -271,4 +271,6 @@ Lataa musiikkikirjasto uudelleen, käytä välimuistissa olevia tunnisteita kun mahdollista Rajaa kaikki albumikannet 1:1-suhteeseen Tyhjennä tunnistevälimuisti ja lataa musiikkikirjasto kokonaan uudelleen (hitaampi mutta kattavampi) + Kappale + Näytä \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4a6cb7884..822484268 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -157,7 +157,7 @@ Surveillance de votre bibliothèque musicale pour les changements… Couvertures arrondies Activer les coins arrondis sur des éléments d\'interface utilisateur supplémentaires (nécessite que les couvertures d\'album soient arrondies) - Descendant + Descendant Etat restauré Personnaliser les commandes et le comportement de l\'interface utilisateur Passer au suivant @@ -169,9 +169,9 @@ Recharger la bibliothèque musicale chaque fois qu\'elle change (nécessite une notification persistante) Esperluette (&) Playlist - Lors de la lecture à partir des détails de l\'élément + Lors de la lecture à partir des détails de l\'élément Gardez la lecture aléatoire lors de la lecture d\'une nouvelle chanson - Lire à partir de l\'élément affiché + Lire à partir de l\'élément affiché N\'oubliez pas de mélanger Contrôlez le chargement de la musique et des images Musique @@ -185,18 +185,18 @@ Couvertures originales (téléchargement rapide) Configurer le son et le comportement de lecture Listes de lecture - Lors de la lecture depuis la bibliothèque + Lors de la lecture depuis la bibliothèque Séparateurs multi-valeurs Rechargement automatique - Jouer à partir de toutes les chansons - Jouer de l\'artiste - Jouer à partir du genre + Jouer à partir de toutes les chansons + Jouer de l\'artiste + Jouer à partir du genre Virgule (,) Point-virgule (;) Ignorer les fichiers audio qui ne sont pas de la musique, tels que les podcasts Avertissement : L\'utilisation de ce paramètre peut entraîner l\'interprétation incorrecte de certaines balises comme ayant plusieurs valeurs. Vous pouvez résoudre ce problème en préfixant les caractères de séparation indésirables avec une barre oblique inverse (\\). Exclure non-musique - Lire depuis l\'album + Lire depuis l\'album Barre oblique (/) Plus (+) Vider l\'état de lecture précédemment enregistré (si il existe) @@ -301,4 +301,5 @@ Recadrer toutes les pochettes d\'album au format 1:1 Chanson Voir + Jouer la chanson par elle-même \ No newline at end of file diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 1ba5cedaf..5c3288e6a 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -8,7 +8,7 @@ Axustes Claro Escuro - Cando se está a reproducir dende a biblioteca + Cando se está a reproducir dende a biblioteca Artista Un reproductor de música simple e racional para android. Sinxelo @@ -82,17 +82,17 @@ Recarga a biblioteca de música cando cambia (require unha notificación persistente) Acción da notificación personalizada Saltar ao seguinte - Reproducir dende xénero + Reproducir dende xénero Manter a mestura ao reproducir unha canción nova Contido Modo de repetición Comportamento - Reproducir dende artista + Reproducir dende artista Lembrar a mestura Música - Reproducir dende o elemento que se mostra - Reproducir dende todas as cancións - Reproducir dende álbum + Reproducir dende o elemento que se mostra + Reproducir dende todas as cancións + Reproducir dende álbum Recarga automática Ignorar arquivos de audio que non sexan música, como os pódcasts Todas as cancións @@ -124,7 +124,7 @@ EP en directo EP remix Ascendente - Descendente + Descendente Ecualizador Aleatorio seleccionado Frecuencia de mostraxe @@ -138,7 +138,7 @@ Pestanas de biblioteca Cambiar a visibilidade e a orde das pestanas da biblioteca Acción personalizada da barra de reprodución - Cando se reproduce dende os detalles + Cando se reproduce dende os detalles Controla como se carga a música e as imaxes Separadores de varios valores Configura caracteres que denotan múltiples valores da etiqueta diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index b4dc11d12..b4014ff97 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -63,7 +63,7 @@ अगला चलाएं फ़ाइल का नाम लायब्रेरी टैब्स - एल्बम से चलाएं + एल्बम से चलाएं सामग्री %d चयनित प्रारूप @@ -85,8 +85,8 @@ गतिशील लुक और फील अतिरिक्त UI तत्वों पर गोल कोनों को सक्षम करें (एल्बम कवर को गोल करने की आवश्यकता है) - दिखाए गए आइटम से चलाएँ - लाइब्रेरी से चलाते समय + दिखाए गए आइटम से चलाएँ + लाइब्रेरी से चलाते समय संगीत लाइब्रेरी को फिर से लोड करें जब भी यह बदलता है (स्थाई नोटीफिकेशन की आवश्यकता होती है) ऑडियो फ़ाइलों को अनदेखा करें जो संगीत नहीं हैं, जैसे कि पॉडकास्ट लाइव संकलन @@ -96,10 +96,10 @@ प्लेलिस्ट प्लेलिस्टें गोल मोड - सभी गीतों से चलाएं + सभी गीतों से चलाएं %s हटाएँ\? इसे पूर्ववत नहीं किया जा सकता। लोड किए गए गाने: %d - अवरोही + अवरोही चयनित चलाएँ फेरबदल का चयन किया गया स्थिति साफ की गई @@ -109,7 +109,7 @@ UI नियंत्रण और व्यवहार अनुकूलित करें कलाकार लोड किए गए: %d कस्टम प्लेबैक बार एक्शन - आइटम विवरण से चलाते समय + आइटम विवरण से चलाते समय लाइव एल्बम रीमिक्स एल्बम लाइव EP @@ -165,14 +165,14 @@ गीत के गुणधर्म डिस्पले कस्टम नोटीफिकेशन एक्शन - कलाकार से चलाएं - शैली से चलाएं + कलाकार से चलाएं + शैली से चलाएं फेरबदल याद रखें नया गाना बजाते समय फेरबदल करते रहें मल्टी-मूल्य विभाजक लोड की गई शैलियाँ: %d लोड किए गए एल्बम: %d - आपकी संगीत लाइब्रेरी लोड कर रहे हैं... (%1$d/%2$d) + आपकी संगीत लाइब्रेरी लोड कर रहे हैं… (%1$d/%2$d) %d kbps आपकी संगीत लाइब्रेरी लोड कर रहे हैं… प्लेलिस्ट बनाई गई @@ -187,7 +187,7 @@ स्लैश (/) -%.1f dB संपादन %s - Plus (+) + पलॅस (+) ऐंपरसैंड (&) मोड एल्बम कवर @@ -298,4 +298,5 @@ गुलाबी बुद्धिमान छंटाई संख्याओं या \"the\" जैसे शब्दों से शुरू होने वाले नामों को सही ढंग से क्रमबद्ध करें (अंग्रेजी भाषा के संगीत के साथ सबसे अच्छा काम करता है) + इसी गीत को चलाएं \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 6be80a5d0..1ce8b10cd 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -82,9 +82,9 @@ Prilagođavanje s oznakama Prilagođavanje bez oznaka Upozorenje: Postavljanje pretpojačala na visoke razine može uzrokovati vrhunce tonova u nekim zvučnim zapisima. - Kada se reproducira iz zbirke - Kada se reproducira iz detalja predmeta - Reproduciraj iz svih pjesama + Kada se reproducira iz zbirke + Kada se reproducira iz detalja predmeta + Reproduciraj iz svih pjesama Aktualiziraj glazbu Ponovo učitaj glazbenu biblioteku, koristeći predmemorirane oznake kada je to moguće Mape glazbe @@ -191,10 +191,10 @@ Premotaj prije preskakanja natrag Spremi trenutno stanje reprodukcije Preskoči na sljedeću pjesmu - Reproduciraj iz prikazanog predmeta + Reproduciraj iz prikazanog predmeta Zapamti miješanje glazbe Vrati prethodno spremljeno stanje reprodukcije (ako postoji) - Reproduciraj iz albuma + Reproduciraj iz albuma Pauziraj čim se pjesma ponovi Premotaj prije vraćanja na prethodnu pjesmu Reproduciraj ili pauziraj @@ -230,7 +230,7 @@ Sakrij suradnike Isključeno Isključi sve što nije glazba - Reproduciraj iz izvođača + Reproduciraj iz izvođača Visoka kvaliteta Brzo Zanemari sve audio datoteke koje nisu glazba, npr. podcast datoteke @@ -249,13 +249,13 @@ Odabrano: %d Promiješaj odabrane Reproduciraj odabrane - Reproduciraj iz žanra + Reproduciraj iz žanra Wiki %1$s, %2$s Resetiraj ReplayGain izjednačavanje glasnoće Mape - Silazni + Silazni Promijenite temu i boje aplikacije Prilagodite kontrole i ponašanje korisničkog sučelja Upravljajte učitavanjem glazbe i slika diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 2717d6868..6c0b11c88 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -74,7 +74,7 @@ Remix EP Név Dátum - Csökkenő + Csökkenő Kiválasztott lejátszása Új lejátszólista Ismeretlen műfaj @@ -86,7 +86,7 @@ Visszajátszás Szülő útvonal Mappa eltávolítása - Playlistához ad + Lejátszólistához ad Formátum Wiki OK @@ -122,13 +122,13 @@ Visszatekerés az előző dalra való ugrás előtt Figyelem: Az előerősítő magas pozitív értékre módosítása egyes hangsávoknál csúcsosodást eredményezhet. Könyvtár - Kitartás + Állapot Lejátszólista Lejátszólisták Töröl Zenelejátszás megtekintése és vezérlése Zenei könyvtár betöltése… - Playlistához adva + Lejátszólistához adva Visszatekerés visszaugrás előtt Ismétlés szünet %s törlése\? Ez nem fordítható vissza. @@ -145,8 +145,8 @@ Kiválasztottak keverése UI vezérlők és viselkedés testreszabása A könyvtárfülek láthatóságának és sorrendjének módosítása - A tétel részleteiből történő lejátszáskor - Lejátszás albumból + A tétel részleteiből történő lejátszáskor + Lejátszás albumból A zene és a képek betöltésének vezérlése Képek Időtartam @@ -175,8 +175,8 @@ Alaphelyzet Állapot törölve Fejlesztő Alexander Capehart - Lejátszás az összes dalból - Lejátszás műfajból + Lejátszás az összes dalból + Lejátszás műfajból Tartalom A zenei könyvtár újratöltése, ha változik (állandó értesítést igényel) Zene könyvtárak @@ -200,20 +200,20 @@ Állapot törlés nem lehetséges Állapot mentés nem lehetséges Keverés minden dalból - Ogg hang + Ogg audio Megjelenítés Hangsáv Szerkeszt Lemez - Playlista létrehozva + Lejátszólista létrehozva Fekete téma Lekerekített sarkok engedélyezése további UI elemeken (az albumborítók lekerekítése szükséges) Könyvtár fülek Mód Free Lossless Audio Codec (FLAC) Beállítás címkékkel - A könyvtárból történő lejátszáskor - Lejátszás a megjelenő elemről + A könyvtárból történő lejátszáskor + Lejátszás a megjelenő elemről %d kbps Betöltött műfaj: %d Zene betöltés @@ -245,18 +245,18 @@ %d előadó %d előadó - Equalizer + Ekvalizer Könyvtár statisztika Playlista átnevezve Playlista törölve Ugrás a következőre - Lejátszás előadótól + Lejátszás előadótól Zene A nem zenei fájlok, például podcastok figyelmen kívül hagyása Több címkeértéket jelölő karakterek konfigurálása Vessző (,) - Ponosvessző (;) - És (&) + Pontosvessző (;) + És jel (&) Intelligens rendezés Közreműködők elrejtése Csak az albumon közvetlenül feltüntetett előadók megjelenítése (a jól címkézett könyvtárakban működik a legjobban) @@ -294,8 +294,9 @@ Auxio ikon Nincs lemez %s szerkesztése - Kényszerített négyzet alakú albumborítók + Négyzet alakú albumborítók Az összes albumborító 1:1 arányra vágása Dal Megnéz + Dal lejátszása önmagában \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index fdd5eb194..525ed9115 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -93,8 +93,8 @@ Pre-amp diterapkan ke penyesuaian yang ada selama pemutaran Penyesuaian dengan tag Peringatan: Mengubah pre-amp ke nilai positif yang tinggi dapat mengakibatkan puncak pada beberapa trek audio. - Putar dari item yang ditampilkan - Putar dari semua lagu + Putar dari item yang ditampilkan + Putar dari semua lagu Tetap mengacak saat memutar lagu baru Jeda pada pengulangan Putar balik sebelum melompat ke belakang @@ -133,17 +133,17 @@ Album yang dimuat: %d Artis yang dimuat: %d Utamakan album jika ada yang diputar - Saat diputar dari pustaka - Putar dari album + Saat diputar dari pustaka + Putar dari album Ubah mode pengulangan Gambar Artis untuk %s - Saat diputar dari keterangan item + Saat diputar dari keterangan item Musik tidak akan dimuat dari folder yang Anda tambahkan. Hapus lagu antrian ini Hapus kueri pencarian Penyesuaian tanpa tag Folder musik - Putar dari artis + Putar dari artis Mode Auxio memerlukan izin untuk membaca perpustakaan musik Anda Loncat ke lagu terakhir @@ -193,7 +193,7 @@ Kualitas tinggi Titik koma (;) Wiki - Putar dari aliran + Putar dari aliran Aliran Sampul album Nonaktif diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 84ee01c8f..483d5b02f 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -5,12 +5,12 @@ Vedi e gestisci la riproduzione musicale Riprova - Permetti + Autorizza Generi Artisti Album - Canzoni - Tutte le canzoni + Brani + Tutte i brani Cerca Filtro Tutto @@ -23,9 +23,9 @@ Ora in riproduzione Riproduci Mescola - Riproduci da tutte le canzoni - Riproduci dall\'album - Riproduci dall\'artista + Riproduci da tutti i brani + Riproduci dall\'album + Riproduci dall\'artista Coda Riproduci successivo Accoda @@ -66,13 +66,13 @@ Preferisci album Preferisci l\'album se in riproduzione Comportamento - Quando in riproduzione dalla libreria + Quando in riproduzione dalla libreria Mantieni mescolamento - Mantiene il mescolamento anche se una nuova canzone è selezionata + Mantiene il mescolamento anche se un nuovo brano è selezionato Riavvolgi prima di saltare indietro - Riavvolge prima di saltare alla traccia precedente + Riavvolge prima di saltare al brano precedente Pausa alla ripetizione - Pausa quando una canzone si ripete + Pausa quando un brano si ripete Contenuti Salva stato riproduzione Salva lo stato di riproduzione corrente @@ -89,13 +89,13 @@ Canzone %d Riproduci o pausa - Salta alla canzone successiva - Salta alla canzone precedente + Salta a brano successivo + Salta a ultimo brano Cambia modalità ripetizione Attiva o disattiva mescolamento - Mescola tutte le canzoni - Rimuove questa canzone della coda - Muove questa canzone della coda + Mescola tutti i brani + Rimuovi questo brano + Sposta questo brano Muove questa scheda Cancella la query di ricerca Rimuovi cartella @@ -128,15 +128,15 @@ Marrone Grigio - Canzoni trovate: %d + Brani trovati: %d Album trovati: %d Artisti trovati: %d Generi trovati: %d Durata totale: %s - %d canzone - %d canzoni - %d canzoni + %d brano + %d brani + %d brani %d album @@ -154,14 +154,14 @@ +%.1f dB %d Hz Caricamento libreria musicale… (%1$d/%2$d) - Quando in riproduzione dai dettagli dell\'elemento + Quando in riproduzione dai dettagli dell\'elemento Attenzione: impostare valore positivi alti può provocare distorsioni su alcune tracce. Regolazione senza tag Mescola Mescola tutto Regolazione con tag Il pre-amp è applicato alla regolazione esistente durante la riproduzione - Riproduci dall\'elemento mostrato + Riproduci dall\'elemento mostrato Gestisci le cartelle da dove caricare la musica Cartelle musica Escludi @@ -174,13 +174,13 @@ Caricamento musica Caricamento libreria musicale… Durata - Numero canzoni + Numero brani Disco Traccia OK Frequenza di campionamento Vedi proprietà - Proprietà canzone + Proprietà brano Nome file Directory superiore Formato @@ -239,7 +239,7 @@ Attenzione: potrebbero verificarsi degli errori nella interpretazione di alcuni tag con valori multipli. Puoi risolvere aggiungendo come prefisso la barra rovesciata (\\) ai separatori indesiderati. E commerciale (&) Raccolte live - Raccolta di remix + Raccolta remix Mix DJ Mix DJ Alta qualità @@ -258,7 +258,7 @@ Mescola selezionati Riproduci selezionati %d selezionati - Riproduci dal genere + Riproduci dal genere Wiki %1$s, %2$s Ripristina @@ -274,7 +274,7 @@ Persistenza Personalizza controlli e comportamento dell\'UI Configura comportamento di suono e riproduzione - Discendente + Discendente Playlist Playlist Ordinazione intelligente @@ -284,8 +284,8 @@ Nuova playlist Aggiungi a playlist Playlist creata - Aggiunto alla playlist - Niente canzoni + Aggiunto a playlist + Nessun brano Playlist %d Elimina Eliminare la playlist\? @@ -301,6 +301,7 @@ Modifica di %s Forza copertine album quadrate Adatta tutte le copertine degli album ad una visualizzazione 1:1 - Canzone + Brano Visualizza + Riproduci brano da solo \ No newline at end of file diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index f3b64bdc3..31b5677fd 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -36,7 +36,7 @@ תאריך הוספה מיון עולה - יורד + יורד מושמע כעת איקוולייזר ניגון @@ -87,13 +87,13 @@ דילוג לבא מצב חזרה התנהגות - כאשר מנוגן מהספרייה - כאשר מנוגן מפרטי הפריט - ניגון מהפריט המוצג - ניגון מכל השירים - ניגון מאלבום - ניגון מהאומן - ניגון מסוגה + כאשר מנוגן מהספרייה + כאשר מנוגן מפרטי הפריט + ניגון מהפריט המוצג + ניגון מכל השירים + ניגון מאלבום + ניגון מהאומן + ניגון מסוגה לזכור ערבוב המשך ערבוב בעת הפעלת שיר חדש תוכן diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index d1b62413b..21849864b 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -54,13 +54,13 @@ カラースキーム 黒基調 再生状態を復元 - 表示されたアイテムから再生 + 表示されたアイテムから再生 再生停止 ファイル名 追加した日付け サンプルレート - 降順 + 降順 再生 シャフル 選択曲をシャフル @@ -151,11 +151,11 @@ ヘッドセット接続時に常時再生開始 (動作しない機種あり) ヘッドセット自動再生 リプレイゲイン - すべての曲から再生 - アルバムから再生 - アーティストから再生 - ライブラリからの再生時 - アイテム詳細からの再生時 + すべての曲から再生 + アルバムから再生 + アーティストから再生 + ライブラリからの再生時 + アイテム詳細からの再生時 音楽以外を除外 ここに追加したフォルダからのみ音楽が読み込まれます。 前回保存された再生状態がある場合、再生状態を復元 @@ -227,7 +227,7 @@ 次ヘスキップ 繰り返しモード - ジャンルから再生 + ジャンルから再生 新しい曲の再生時にシャフルを保持 ダイナミック 再生状態を解除できません diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 7460a0012..6716d4d77 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -27,9 +27,9 @@ 지금 재생 중 재생 셔플 - 모든 곡에서 재생 - 앨범에서 재생 - 아티스트에서 재생 + 모든 곡에서 재생 + 앨범에서 재생 + 아티스트에서 재생 대기열 다음 곡 재생 대기열에 추가 @@ -78,7 +78,7 @@ 태그 없이 조정 주의: 프리앰프를 높게 설정하면 일부 소리 트랙이 왜곡될 수 있습니다. 동작 - 라이브러리에서 재생할 때 + 라이브러리에서 재생할 때 무작위 재생 기억 새로운 곡을 재생할 때 무작위 재생 유지 이전 곡으로 가기 전에 되감기 @@ -178,7 +178,7 @@ DJ믹스 이퀄라이저 셔플 - 표시된 항목에서 재생 + 표시된 항목에서 재생 음악 라이브러리를 불러오는 중… 재생 상태 지우기 재생 상태 복원 @@ -238,7 +238,7 @@ 음악 라이브러리를 불러오는 중… (%1$d/%2$d) 장르 경고: 이 설정을 사용하면 일부 태그가 여러 값을 갖는 것으로 잘못 해석될 수 있습니다. 구분자로 읽히지 않도록 하려면 해당 구분자 앞에 백슬래시 (\\)를 붙입니다. - 항목 세부 정보에서 재생할 때 + 항목 세부 정보에서 재생할 때 음악 라이브러리의 변경사항을 추적하는 중… 다음 곡으로 건너뛰기 팟캐스트와 같이 음악이 아닌 소리 파일 무시 @@ -256,7 +256,7 @@ %d 선택됨 재설정 위키 - 장르에서 재생 + 장르에서 재생 %1$s, %2$s 리플레이게인 사운드 및 재생 동작 구성 @@ -270,7 +270,7 @@ 지속 동작 UI 제어 및 동작 커스텀 - 내림차순 + 내림차순 재생목록 재생목록 %s의 재생 목록 이미지 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 946097b67..caa995e31 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -9,10 +9,10 @@ Pavadinimas Metai Trukmė - Dainų skaičius + Dainos skaičius Diskas Pridėta data - Kylantis + Didėjantis Groti kitą Pridėti į eilę Eilė @@ -44,22 +44,22 @@ Formatas Versija Nustatymai - Temos - Naudokti grynai juodą tamsią temą - Paprastas, racionalus „Android“ muzikos grotuvas. + Tema + Naudoti grynai juodą tamsią temą + Paprastas, racionalus Android muzikos grotuvas. Muzika kraunama - Peržiūrėti ir valdyti muzikos grojimą + Peržiūrėk ir valdyk muzikos grojimą Žanrai - Bandykite dar kartą + Pakartoti Suteikti Kraunama muzika - Kraunama jūsų muzikos biblioteka… + Kraunamas tavo muzikos biblioteka… Bibliotekos statistika Rožinis Albumas Mini albumas Singlas - Atlikėjas + Atlikėjas (-a) Nežinomas žanras Nėra datos Raudona @@ -71,22 +71,22 @@ Nežinomas atlikėjas Albumo viršelis Giliai violetinė - Stebėjima muzikos biblioteka - Stebima jūsų muzikos biblioteka dėl pakeitimų… + Stebėjimas muzikos biblioteka + Stebima tavo muzikos biblioteko dėl pakeitimų… Maišyti - Išmaišyti viską - Būsena atkurta + Maišyti viską + Atkurta būsena Išsaugota būsena Atšaukti Šaltinio kodas - Rodyti - „ReplayGain“ strategija + Rodinys + ReplayGain strategija Singlai Gerai - Įgalinti papildomų vartotojo sąsajos elementų suapvalintus kampus (reikia, kad albumo viršeliai būtų suapvalinti) + Įjungti suapvalintų kampų papildomiems UI elementams (reikia, kad albumo viršeliai būtų suapvalinti) Garso takelis Garso takeliai - Garso + Garsas Apvalus režimas Pasirinktinis pranešimo veiksmas MPEG-1 garsas @@ -102,8 +102,8 @@ Remiksai Arbatžolė Geltona - Išplėstinis Garso Kodavimas (AAC) - Nemokamas Be Nuostolių Garso Kodekas (FLAC) + Išplėstinis garso kodavimas (AAC) + Nemokamas be nuostolių garso kodekas (FLAC) %d daina %d dainos @@ -121,27 +121,27 @@ Gyvai albumas Remikso albumas Gyvai - Visada pradėti groti, kai prijungtos ausinės (gali veikti ne visuose įrenginiuose) + Visada pradėti groti, kai ausinės yra prijungtos (gali neveikti visuose įrenginiuose) Ogg garsas Sukūrė Alexanderis Capehartas - Pageidaujamas takeliui + Pageidauti takelį Jokių aplankų Šis aplankas nepalaikomas Groti arba pristabdyti - Peršokti į kitą dainą - Peršokti į paskutinę dainą + Praleisti į kitą dainą + Praleisti į paskutinę dainą Mikstapas Mikstapai Bibliotekos skirtukai Keisti bibliotekos skirtukų matomumą ir tvarką - Pageidaujamas albumui - Pageidaujamas albumui, jei vienas groja + Pageidauti albumui + Pageidauti albumui, jei vienas groja Jokią programą nerasta, kuri galėtų atlikti šią užduotį Auxio piktograma Perkelti šią dainą Perkelti šį skirtuką - Muzikos krovimas nepavyko - Auxio reikia leidimo skaityti jūsų muzikos biblioteką + Muzikos įkrovimas nepavyko + Auxio reikia leidimo skaityti tavo muzikos biblioteką Diskas %d +%.1f dB -%.1f dB @@ -152,66 +152,66 @@ Kompiliacija Prisiminti maišymą Palikti maišymą įjungtą, kai groja nauja daina - Persukti prieš šokant atgal - Persukti atgal prieš peršokant į ankstesnę dainą + Persukti prieš praleistant atgal + Persukti atgal prieš praleistant į ankstesnę dainą Pauzė ant kartojamo - Kai grojant iš bibliotekos - Kai grojant iš elemento detalių + Kai grojant iš bibliotekos + Kai grojant iš elemento detalių Pašalinti aplanką Žanras - Ieškokite savo bibliotekoje… + Ieškok savo bibliotekoje… Ekvalaizeris Režimas - Automatinis krovimas + Automatinis įkrovimas Jokios muzikos nerasta Sustabdyti grojimą Nėra takelio - Pereiti prie kitos + Praleisti į kitą Automatinis ausinių grojimas Kartojimo režimas Atidaryti eilę - Išvalyti paieškos užklausą - Muzika nebus įkeliama iš pridėtų aplankų jūs pridėsite. + Išvalyti paieškos paraišką + Muzika nebus įkeliama iš pridėtų aplankų, kurių tu pridėsi. Įtraukti Pašalinti šią dainą - Groti iš visų dainų - Groti iš parodyto elemento - Groti iš albumo - Groti iš atlikėjo + Groti iš visų dainų + Groti iš parodyto elemento + Groti iš albumo + Groti iš atlikėjo Išvalyta būsena Neįtraukti - Muzika bus įkeliama iš aplankų jūs pridėsite. + Muzika bus įkeliama iš aplankų, kurių tu pridėsi. %d Hz Perkrauti muzikos biblioteką, kai ji pasikeičia (reikia nuolatinio pranešimo) Įkeltos dainos: %d - Įkrauti žanrai: %d + Įkeltos žanrai: %d Įkeltos albumai: %d - Įkrauti atlikėjai: %d - Kraunama jūsų muzikos biblioteka… (%1$d/%2$d) + Įkeltos atlikėjai: %d + Kraunamas tavo muzikos biblioteka… (%1$d/%2$d) Maišyti visas dainas - Elgesys - Įspėjimas: Keičiant išankstinį stiprintuvą į didelę teigiamą vertę, kai kuriuose garso takeliuose gali atsirasti pikų. - Albumo viršelis, skirtas %s - Atlikėjo vaizdas, skirtas %s - Nėra grojamos muzikos - Pauzė, kai daina kartojasi + Personalizuotas + Įspėjimas: Keičiant išankstinį stiprintuvą į didelę teigiamą vertę, kai kuriuose garso takeliuose gali atsirasti tarpų. + Albumo viršelis %s + Atlikėjo vaizdas %s + Nėra grojančio muzikos + Sustabdyti, kai daina kartojasi Turinys Muzikos aplankai Atnaujinti muziką Perkrauti muzikos biblioteką, naudojant talpyklos žymes, kai įmanoma Pasirinktinis grojimo juostos veiksmas Nepavyko atkurti būsenos - „ReplayGain“ išankstinis stiprintuvas + ReplayGain išankstinis stiprintuvas Išsaugoti grojimo būseną - Tvarkykite, kur muzika turėtų būti įkeliama iš - Žanro vaizdas, skirtas %s + Tvarkyti, kur muzika turėtų būti įkeliama iš + Žanro vaizdas %s Įjungti maišymą arba išjungti Takelis %d Keisti kartojimo režimą Indigos %d kbps - DJ\'ų Miksai - DJ\'o Miksas + DJ miksai + DJ miksas Gyvai kompiliacija Remikso kompiliacija Pagrindinis aplankas @@ -222,18 +222,18 @@ Ampersandas (&) Albumų viršeliai Išjungta - Greitai + Greitis Išsaugoti dabartinę grojimo būseną dabar Išvalyti grojimo būseną - Konfigūruokite simbolius, kurie nurodomos kelias žymių reikšmes + Konfigūruoti simbolius, kurie nurodo kelias žymių reikšmes Kablelis (,) Reguliavimas be žymų Įspėjimas: Naudojant šį nustatymą, kai kurios žymos gali būti neteisingai interpretuojamos kaip turinčios kelias reikšmes. Tai galima išspręsti prieš nepageidaujamus skiriamuosius ženklus naudojant atgalinį brūkšnį (\\). Kabliataškis (;) - Aukšta kokybė + Aukštos kokybės Atkurti grojimo būseną Neįtraukti nemuzikinių - Ignoruoti garso failus, kurie nėra muzika, pavyzdžiui, podcast\'us + Ignoruoti garso failus, kurie nėra muzika, pvz., tinklalaides Išankstinis stiprintuvas taikomas esamam reguliavimui grojimo metu Reguliavimas su žymėmis Atkurti anksčiau išsaugotą grojimo būseną (jei yra) @@ -242,44 +242,43 @@ Nepavyko išvalyti būsenos Nepavyko išsaugoti būsenos - %d atlikėjas + %d atlikėjas (-a) %d atlikėjai - %d albumų %d atlikėjų Perskenuoti muziką Išvalyti žymių talpyklą ir pilnai perkrauti muzikos biblioteką (lėčiau, bet labiau išbaigta) - %d Pasirinkta - Groti pasirinktą + %d pasirinkta + Pasirinktas grojimas Pasirinktas maišymas - Groti iš žanro + Groti iš žanro Viki %1$s, %2$s Nustatyti iš naujo Biblioteka Elgesys - Pakeisti programos temą ir spalvas - Valdyti, kaip muzika ir vaizdai įkeliama - Konfigūruoti garso ir grojimo elgesį - Naudotojo UI ir elgsenos pritaikymas + Pakeisk programos temą ir spalvas + Valdyk, kaip muzika ir vaizdai įkeliami + Konfigūruok garso ir grojimo elgesį + Pritaikyk UI valdiklius ir elgseną Muzika Vaizdai Grojimas - „ReplayGain“ + ReplayGain Aplankalai - Atkaklumas - Mažėjantis - Ignoruoti tokius žodžius kaip „the“, kai rūšiuojama pagal pavadinimą (geriausiai veikia su anglų kalbos muzika) - Ignoruoti straipsnius rūšiuojant + Pastovumas + Mažėjantis + Teisingai surūšiuoti pavadinimus, kurie prasideda skaičiais arba žodžiais, tokiais kaip „the“ (geriausiai veikia su anglų kalbos muzika) + Išmanusis rūšiavimas Grojaraštis Grojaraščiai Grojaraščio vaizdas %s Sukurti naują grojaraštį Naujas grojaraštis - Įtraukti į grojaraštį - Įtraukta į grojaraštį + Pridėti į grojaraštį + Pridėta į grojaraštį Ištrinti - Ištrinti %s\? To negalima atšaukti. + Ištrinti %s\? To negalima atkurti. Pervadinti Pervadinti grojaraštį Ištrinti grojaraštį\? @@ -292,5 +291,10 @@ Ištrintas grojaraštis Nėra disko Redaguojama %s - Rodoma + Pasirodo + Daina + Peržiūrėti + Apkarpyti visus albumų viršelius iki 1:1 kraštinių koeficiento + Priversti kvadratinių albumų viršelius + Groti dainą pačią \ No newline at end of file diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 329f2ae1f..82f0ffe92 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -90,10 +90,19 @@ കലാകാരനിലേക്ക് പോകുക സവിശേഷതകൾ കാണുക സ്ഥിതി സംരക്ഷിച്ചു - അവരോഹണം + അവരോഹണം സ്ഥിതി പുനഃസ്ഥാപിച്ചു വിക്കി സ്ഥിതി മായ്ച്ചു തത്സമയം തത്സമയ സമാഹാരം + ഗീതം + ഇല്ലാതാക്കുക + പേരുമാറ്റുക + തിരുത്തുക + സ്വയമേവ + വ്യക്തിപരമാക്കുക + കാണുക + പങ്കിടുക + ദൃശ്യമാകുന്നു \ No newline at end of file diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 3a0906840..5913008c0 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -1,2 +1,295 @@ - \ No newline at end of file + + Legg til i kø + Personaliser + Artister innlastet: %d + Legg til i spilleliste + Kildekode + Lisenser + Lys + Automatisk + ReplayGain + Gjenoppfrisk musikk + Justering uten etiketter + Flytt denne fanen + Tøm søk + Auxio-ikon + Åpne køen + Ingen dato + MPEG-1-lyd + Brun + Spor + EP-er + EP + Lydspor + Lydspor + Mikstaper + Mikstape + Remikser + Artist + Artister + Sjanger + Sjangere + Ny spilleliste + Søk + Kompilasjoner + Kompilasjon + Live-kompilasjon + Remiks-kompilasjon + Bidrar på + Alle + Navn + Varighet + Sporantall + + Spill neste + Omstokking valgt + Bibliotek + Kunne ikke lagre tilstand + + %d artist + %d artister + + + %d spor + %d spor + + Filter + Spilleliste + Spillelister + Slett + Disk + Gå til artist + Gå til album + Vis + Del + Størrelse + Bitrate + Samplerate + Omstokking + Omstokk alt + Avbryt + Om + Vis og kontroller musikkavspilling + Tillagt i kø + Gjentagelsesmodus + Tilpass grensesnittskontroller og adferd + Utseende og adferd + Avrundede hjørner i ytterligere grensesnittselementer (krever at albumsomslag er avrundet) + Behold omstokking ved avspilling av et nytt spor + Husk omstokking + Skråstrek (/) + Plusstegn (+) + Skjul bidragsytere + Bilder + Albumsomslag + Rask + Høy kvalitet + Alltid start avspilling når hodetelefoner kobles til (trenger ikke å virke på alle enheter) + Sett opp lyd- og avspillingsadferd + Lyd + ReplayGain-strategi + Håndter hvor musikk lastes inn fra + Forforsterkning brukes for eksisterende justering under avspilling + Modus + Lagre nåværende avspillingstilstand nå + Lagre avspillingstilstand + Tøm etiketthurtiglager og last inn hele musikkbiblioteket igjen (tregere, men mer fullstendig) + Vedvarende + Spor %d + Albumsomslag + Skru omstokking på eller av + Fjern dette sporet + Flytt dette sporet + Ingen disk + Ingen spor + Avansert audio-koding (AAC) + Dynamisk + %d valgt + Laster inn musikkbiblioteket ditt … (%1$d/%2$d) + %d Hz + Slett %s for godt\? + Gjenopprett tidligere lagret avspillingstilstand (hvis noen) + MPEG-4-lyd + Spilleliste %d + Cyanblå + Spor innlastet: %d + Dato + Endre drakten og programfargene + Mapper + Tøm avspillingstilstand + Fjern tidligere lagret avspillingstilstand (hvis noen) + Gul + Intelligent sortering + Gi nytt navn + Gi spillelisten nytt navn + Slett spilleliste\? + OK + Tilstand lagret + Versjon + Wiki + Tilbakestill + Legg til + Tilstand fjernet + Tilstand gjenopprettet + Fargedrakt + Svart drakt + Ifør helsvart drakt + Drakt + Mørk + Musikk + Utelat ikke-musikk + Komma (,) + Semikolon (;) + Automatisk gjeninnlasting + Ignorer lydfiler som ikke er musikk, som f-eks. nettradioopptak + Multiverdi-inndelere + Sett opp tegn for inndeling av flere etikett-verdier + Kun vis artister som er kreditert direkte på album (fungerer best med godt etikettmerkede bibliotek) + Reskann musikk + Musikk vil kun innlastes fra mappene du legger til. + Last inn musikkbiblioteket igjen, ved bruk av hurtiglagrede etiketter når mulig + Ingen mapper + Kunne ikke gjenopprette tilstand + Kunne ikke fjerne tilstand + Album innlastet: %d + Biblioteksstatistikk + Av + Avrundede hjørner + ReplayGain-forforsterkning + Justering med etiketter + Adferd + Innhold + Musikkmapper + Gjeninnlast musikkbibliotek når det endrer seg (krever vedvarende merknad) + Kunne ikke laste inn musikk + Denne mappen støttes ikke + Hopp til neste spor + Hopp til siste spor + Omstokk alle spor + Fjern mappe + Ukjent sjanger + Sjangerbilde for %s + Ukjent artist + Sjangere innlastet: %d + Stopp avspilling + Opprett en ny spilleliste + Grønn + Mørkegrønn + Turkis + Rediger + Albumsomslag for %s + Lilla + Blå + Fritt tapsfritt lydkodek (FLAC) + Indigo + Total varighet: %s + DJ-mikser + DJ-miks + Live + Spilles nå + Omstokking + Stigende + Format + Vis egenskaper + Spor-egenskaper + Filnavn + Overnevnt sti + Pause ved gjentagelse + Rød + + %d album + %d album + + Synkende + Spor + Dato tillagt + Sorter + Utviklet av Alexander Capehart + Søk i biblioteket ditt … + Innstillinger + Spilleliste opprettet + Spillelistenavn endret + Holder øye med endringer i musikkbiblioteket ditt … + Spilleliste slettet + Lagt til i spilleliste + Hopp til neste + Egendefinert merknadshandling + Foretrekk spor + Pause når et spor gjentas + Spol tilbake før spor hoppes over + Spol tilbake før hopp til forrige spor + Advarsel: Endring av forforsterkning til høy positiv verdi kan resultere i forvrengning ved høyt lydtrykk på noen spor. + Avspilling + Disk %d + Skjerm + Biblioteksfaner + Endre synlighet og rekkefølgen på biblioteksfaner + Egendefinert avspillingsfelt-handling + Utelat + Inkluder + Musikk vil ikke innlastes fra mappene du legger til. + Ingen spor + Ingen musikk spilles + Oransje + Ved avspilling fra elementsdetaljer + Spill fra album + Spill fra sjanger + Foretrekk album + Gjenopprett avspillingstilstand + Ampersand (&) + Spill sporet for seg selv + Påtving kvadratiske albumsomslag + Korrekt sortering av navn som begynner med tall eller ord som «the» (fungerer best med engelskspråklig musikk) + Beskjær alle albumsomslag til 1:1-sideforhold + Spill av eller pause + Artistbilde for %s + Auxio trenger tilgang til å lese musikkbiblioteket ditt + Rosa + Grå + Redigerer %s + %1$s, %2$s + Limegrønn + -%.1f dB + En enkel, rasjonell musikkspiller for Android. + Prøv igjen + Alle spor + Album + Live-EP + Spill fra alle spor + Laster inn musikk … + Live-singel + Laster inn musikk … + Holder øye med musikkbiblioteket … + Innvilg + Singler + Spor + Album + Live-album + Remiks-album + Remiks-EP + Singel + Remiks-singel + Mørkelilla + +%.1f dB + Tonekontroll + Endre gjentagelsesmodus + Spill + Spill valgte + Lagre + Laster inn musikkbiblioteket ditt … + Ved avspilling fra bibliotek + Spill fra artist + Fant ikke noe musikk + Matroska-lyd + Spill fra vist element + Ogg-lyd + Mørkeblå + Foretrekk album hvis det avspilles + Hodesett-autoavspilling + Spillelistebilde for %s + Kontroller hvordan musikk og bilder innlastes + Installer et program som kan utføre denne handlingen først + Advarsel: Kan forårsake feilaktig tolkning av etiketter som om de har flere verdier. Kan løses ved å innlede uønskede inndelertegn med omvendt skråstrek (\\). + %d kbps + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index e27184725..4d29de5df 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -17,9 +17,9 @@ Oplopend Afspelen Shuffle - Speel van alle nummers - Speel af van album - Speel van artiest + Speel van alle nummers + Speel af van album + Speel van artiest Nu afspelen Wachtrij Afspelen als volgende @@ -48,7 +48,7 @@ Gebruikt een afternatief notification action Audio Gedrag - Bij het afspelen vanuit de bibliotheek + Bij het afspelen vanuit de bibliotheek Onthoud shuffle Houd shuffle aan bij het afspelen van een nieuw nummer Terugspoelen voordat je terugspoelt @@ -149,7 +149,7 @@ Aanpassing met tags Aanpassing zonder tags Er speelt geen muziek - Bij het afspelen van item details + Bij het afspelen van item details Ronde modus Afgeronde hoeken inschakelen voor extra UI-elementen (vereist dat albumhoezen zijn afgerond) Staat gerestaureerd @@ -158,7 +158,7 @@ Headset automatisch afspelen ReplayGain Waarschuwing: Als u de voorversterker op een hoge positieve waarde zet, kan dit bij sommige audiotracks tot pieken leiden. - Afspelen vanaf getoond item + Afspelen vanaf getoond item Afspeelstatus herstellen Herstel de eerder opgeslagen afspeelstatus (indien aanwezig) Kan status niet herstellen @@ -246,7 +246,7 @@ Genre Ampersand (&) Bewerken - Aflopend + Aflopend Kan status niet wissen Afspeellijst-afbeelding voor %s Geen nummers @@ -271,7 +271,7 @@ Verwijderen Scheiders met meerdere waarden Verberg bijdragers - Speel vanuit genre + Speel vanuit genre Datum toegevoegd %1$s, %2$s Afspeellijst %d diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index d8ad7d851..0c83a6bfa 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -75,7 +75,7 @@ ਐਂਡਰੌਇਡ ਲਈ ਇੱਕ ਸਰਲ, ਤਰਕਸੰਗਤ ਸੰਗੀਤ ਪਲੇਅਰ। ਖੋਜੋ ਗੀਤ ਦੀ ਗਿਣਤੀ - ਘਟਦੇ ਹੋਏ + ਘਟਦੇ ਹੋਏ ਚੁਣਿਆ ਹੋਇਆ ਚਲਾਓ ਕਲਾਕਾਰ \'ਤੇ ਜਾਓ ਫਾਈਲ ਦਾ ਨਾਮ @@ -112,13 +112,13 @@ ਅਗਲੇ \'ਤੇ ਜਾਓ ਦੁਹਰਾਓ ਮੋਡ ਵਿਵਹਾਰ - ਜਦੋਂ ਲਾਇਬ੍ਰੇਰੀ ਤੋਂ ਚਲਾਉਂਦੇ ਹਾਂ - ਜਦੋਂ ਆਈਟਮ ਦੇ ਵੇਰਵਿਆਂ ਤੋਂ ਚਲਾਉਂਦੇ ਹਾਂ - ਦਿਖਾਈ ਗਈ ਆਈਟਮ ਤੋਂ ਚਲਾਓ - ਸਾਰੇ ਗੀਤਾਂ ਤੋਂ ਚਲਾਓ - ਐਲਬਮ ਤੋਂ ਚਲਾਓ - ਕਲਾਕਾਰ ਤੋਂ ਖੇਡੋ - ਸ਼ੈਲੀ ਤੋਂ ਖੇਡੋ + ਜਦੋਂ ਲਾਇਬ੍ਰੇਰੀ ਤੋਂ ਚਲਾਉਂਦੇ ਹਾਂ + ਜਦੋਂ ਆਈਟਮ ਦੇ ਵੇਰਵਿਆਂ ਤੋਂ ਚਲਾਉਂਦੇ ਹਾਂ + ਦਿਖਾਈ ਗਈ ਆਈਟਮ ਤੋਂ ਚਲਾਓ + ਸਾਰੇ ਗੀਤਾਂ ਤੋਂ ਚਲਾਓ + ਐਲਬਮ ਤੋਂ ਚਲਾਓ + ਕਲਾਕਾਰ ਤੋਂ ਖੇਡੋ + ਸ਼ੈਲੀ ਤੋਂ ਖੇਡੋ ਸ਼ਫਲ ਯਾਦ ਰੱਖੋ ਗੀਤ ਦੁਹਰਾਉਣ ਤੇ ਰੋਕੋ ਰੀਪਲੇਅ-ਗੇਨ @@ -291,4 +291,5 @@ ਸਾਰੇ ਐਲਬਮ ਕਵਰਾਂ ਨੂੰ 1:1 ਦੇ ਆਕਾਰ ਅਨੁਪਾਤ ਤੱਕ ਕਾਂਟ-ਛਾਂਟ ਕਰੋ ਗੀਤ ਵੇਖੋ + ਇਸੇ ਗੀਤ ਨੂੰ ਚਲਾਓ \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index dbce6c061..a472dc577 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -132,8 +132,8 @@ Korektor Rozmiar Brak folderów - Odtwórz wszystkie utwory - Odtwórz album + Odtwórz wszystkie utwory + Odtwórz album Automatycznie odtwórz muzykę po podłączeniu słuchawek (może nie działać na wszystkich urządzeniach) Odśwież muzykę Odśwież bibliotekę muzyczną używając tagów z pamięci cache, jeśli są dostępne @@ -157,7 +157,7 @@ Tryb powtarzania Ustawienie ReplayGain Preferuj album, jeśli jest odtwarzany - Odtwarzanie z widoku biblioteki + Odtwarzanie z widoku biblioteki Zapisz stan odtwarzania Przecinek (,) Średnik (;) @@ -215,8 +215,8 @@ Regulacja w oparciu o tagi Regulacja bez tagów Wzmocnienie dźwięku przez preamplifier jest nakładane na wcześniej ustawione wzmocnienie podczas odtwarzania - Odtwarzanie z widoku szczegółowego - Odtwórz tylko wybrane + Odtwarzanie z widoku szczegółowego + Odtwórz tylko wybrane Zatrzymaj odtwarzanie, kiedy utwór się powtórzy Muzyka będzie importowana tylko z wybranych folderów. Znaki oddzielające wartości @@ -237,7 +237,7 @@ Kontynuuj odtwarzanie losowe po wybraniu nowego utworu Zaimportowane utwory: %d Ignoruj pliki audio które nie są utworami muzycznymi (np. podcasty) - Odtwórz od wykonawcy + Odtwórz od wykonawcy Wyklucz inne pliki dźwiękowe Okładki albumów Wyłączone @@ -262,7 +262,7 @@ Resetuj Wiki Funkcje - Odtwórz z gatunku + Odtwórz z gatunku Wyczyść pamięć cache z tagami i zaimportuj ponownie bibliotekę (wolniej, ale dokładniej) Zaimportuj ponownie bibliotekę @@ -275,7 +275,7 @@ Muzyka Nie można wyczyścić stanu odtwarzania Nie można zapisać stanu odtwarzania - Malejąco + Malejąco Playlisty Playlista Obraz playlisty %s @@ -302,4 +302,7 @@ Edytowanie %s Przytnij okładki do formatu 1:1 Wymuś kwadratowe okładki + Piosenka + Odtwarzanie utworu samodzielnie + Widok \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 845157184..ec43dcdc8 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -163,8 +163,8 @@ Exibição Ativa cantos arredondados em elementos adicionais da interface do usuário Modo de normalização de volume (ReplayGain) - Reproduzir a partir do item mostrado - Reproduzir de todas as músicas + Reproduzir a partir do item mostrado + Reproduzir de todas as músicas Preferir álbum Prefira o álbum se estiver tocando Recarregamento automático @@ -183,7 +183,7 @@ Monitorando alterações na sua biblioteca de músicas… Abas da biblioteca Gênero - Reproduzir do artista + Reproduzir do artista Restaura a lista de reprodução salva anteriormente (se houver) Ajuste em faixas com metadados Lista limpa @@ -193,7 +193,7 @@ Monitorando a biblioteca de músicas Cantos arredondados Pular para o próximo - Reproduzir do álbum + Reproduzir do álbum Salvar lista de reprodução Limpar lista de reprodução Restaurar lista de reprodução @@ -211,8 +211,8 @@ Single remix Conteúdo Faixa - Ao tocar da biblioteca - Ao tocar a partir dos detalhes do item + Ao tocar da biblioteca + Ao tocar a partir dos detalhes do item Mixtapes Mixtape Remixes @@ -259,7 +259,7 @@ Wiki Redefinir %1$s, %2$s - Tocar a partir do gênero + Tocar a partir do gênero Configure o comportamento de som e reprodução Imagens Mude o tema e as cores do aplicativo @@ -272,7 +272,7 @@ Persistência Comportamento Pastas - Descendente + Descendente Ignorar artigos ao classificar Ignore palavras como \"the\" ao classificar por nome (funciona melhor com músicas em inglês) \ No newline at end of file diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index ff1aa5b04..8140c3ad1 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -71,8 +71,8 @@ %d Álbuns %d Álbuns - Reproduzir a partir do item mostrado - Reproduzir do álbum + Reproduzir a partir do item mostrado + Reproduzir do álbum Imagem do artista para %s Gênero desconhecido Nenhuma faixa @@ -93,7 +93,7 @@ Qualidade alta Ação da barra de reprodução personalizada Modo de repetição - Reproduzir do artista + Reproduzir do artista Pausar na repetição O Auxio precisa de permissão para ler a sua biblioteca de músicas Sem pastas @@ -188,7 +188,7 @@ Estratégia do ganho de repetição Preferir álbum O pré-amplificador é aplicado ao ajuste existente durante a reprodução - Reproduzir de todas as músicas + Reproduzir de todas as músicas Pausa quando uma música se repete Limpe o estado de reprodução salvo anteriormente (se houver) Restaurar o estado de reprodução @@ -239,8 +239,8 @@ Mostrar apenas artistas que foram creditados diretamente no álbum (funciona melhor em músicas com metadados completos) Preferir faixa Pré-amplificação da normalização de volume - Ao tocar a partir dos detalhes do item - Tocar a partir do gênero + Ao tocar a partir dos detalhes do item + Tocar a partir do gênero Retrocede a música antes de voltar para a anterior Recarregar música %1$s, %2$s @@ -250,7 +250,7 @@ Nenhuma lista pode ser restaurada Ícone do Auxio Aleatorizar tudo - Ao tocar da biblioteca + Ao tocar da biblioteca Singles Single Recarrega a biblioteca de músicas usando metadados salvos em cache quando possível @@ -260,7 +260,7 @@ %d artistas Equalização de volume ReplayGain - Descendente + Descendente Mude o tema e as cores do app Personalize os controles e o comportamento da interface do usuário Controle como a música e as imagens são carregadas diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 35c85858b..793b4ec7d 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -137,26 +137,26 @@ Redare selecție Listă de redare Liste de redare - Descrescător + Descrescător Selecție aleatorie aleasă Treceți la următoarea - Redă de la artist - Redă din genul + Redă de la artist + Redă din genul Resetează Wiki Vizualizați și controlați redarea muzicii Schimbă vizibilitatea și ordinea taburilor din bibliotecă Taburi din bibliotecă Nu uita de shuffle - Redă din toate melodiile - În timpul redării din bibliotecă - Redă de la articolul afișat + Redă din toate melodiile + În timpul redării din bibliotecă + Redă de la articolul afișat Conținut Acțiune de notificare personalizată Menține funcția shuffle activată la redarea unei melodii noi Personalizarea acțiunii bării de redare Modul de repetare - Redă din album - În timpul redării de la detaliile articolului + Redă din album + În timpul redării de la detaliile articolului Comportament \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 2dfc2830c..6f311d10a 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -23,9 +23,9 @@ Сейчас играет Играть Перемешать - Играть все композиции - Играть альбом - Играть исполнителя + Играть все композиции + Играть альбом + Играть исполнителя Очередь Играть далее Добавить в очередь @@ -65,7 +65,7 @@ По альбому Предпочитать по альбому, если он воспроизводится Поведение - При воспроизведении из библиотеки + При воспроизведении из библиотеки Запоминать перемешивание Запоминать режим перемешивания для новых треков Сначала перемотать трек @@ -146,8 +146,8 @@ Перемешать Перемешать всё ОК - При воспроизведении из сведений - Воспроизведение с показанного элемента + При воспроизведении из сведений + Воспроизведение с показанного элемента Номер песни Битрейт Диск @@ -264,7 +264,7 @@ Вики Сбросить %1$s,%2$s - Играть жанр + Играть жанр Поведение Выравнивание громкости ReplayGain Музыка @@ -277,7 +277,7 @@ Воспроизведение Папки Состояние воспроизведения - По убыванию + По убыванию Плейлист Плейлисты Обложка плейлиста для %s @@ -305,4 +305,6 @@ Использовать квадратные обложки альбомов Обрезать все обложки альбомов до соотношения сторон 1:1 Песня + Вид + Воспроизвести трек отдельно \ No newline at end of file diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index a00ec585d..e9547d169 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -39,7 +39,7 @@ Spår Datum tillagt Stigande - Fallande + Fallande Nu spelar Utjämnare Spela @@ -124,8 +124,8 @@ Hoppa till nästa Upprepningsmodus Beteende - När spelar från artikeluppgifter - Spela från genre + När spelar från artikeluppgifter + Spela från genre Komma ihåg blandningsstatus Behåll blandning på när spelar en ny låt Kontent @@ -141,11 +141,11 @@ Dölj medarbetare Skärm Bibliotekflikar - När spelar från biblioteket - Spela från visad artikel - Spela från alla låtar - Spela från konstnär - Spela från album + När spelar från biblioteket + Spela från visad artikel + Spela från alla låtar + Spela från konstnär + Spela från album Semikolon (;) Ladda om musikbiblioteket när det ändras (kräver permanent meddelande) Komma (,) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 073c463c3..86915531e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -92,9 +92,9 @@ Etiketsiz ayarla Etiket ile ayarla Uyarı: Ön amfinin yüksek bir pozitif değere değiştirilmesi bazı ses parçalarında pik yapmaya neden olabilir. - Gösterilen öğeden çal - Tüm şarkılardan çal - Albümden çal + Gösterilen öğeden çal + Tüm şarkılardan çal + Albümden çal Müzik klasörleri Müzik yalnızca eklediğiniz klasörlerden yüklenecektir. %s Albümünün kapağı @@ -112,9 +112,9 @@ Kireç Sarı Turuncu - Kitaplıktan çalarken - Öğe ayrıntılarından çalarken - Sanatçıdan çal + Kitaplıktan çalarken + Öğe ayrıntılarından çalarken + Sanatçıdan çal Yüklenen sanatçılar: %d Yüklenen türler: %d %d Hz @@ -248,7 +248,7 @@ Eğik çizgi (/) Kuyruğu aç Tekrar kipi - Türden çal + Türden çal Podcast\'ler gibi müzik olmayan ses dosyalarını yok say Uyarı: Bu ayarın kullanılması bazı etiketlerin yanlışlıkla birden fazla değere sahip olarak yorumlanmasına neden olabilir. Bunu, istenmeyen ayırıcı karakterlerin önüne ters eğik çizgi (\\) koyarak çözebilirsiniz. Müzik olmayanları hariç tut @@ -263,7 +263,7 @@ Oynatma Kütüphane Kalıcılık - Azalan + Azalan Uygulamanın temasını ve renklerini değiştirin Klasörler Arayüz kontrollerini ve davranışını özelleştirin diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 73f6bb64a..a8c4959d6 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -143,14 +143,14 @@ Режим повторення Режим Попередній підсилювач ReplayGain - Відтворити альбом - При відтворенні з бібліотеки + Відтворити альбом + При відтворенні з бібліотеки Віддавати перевагу альбому, якщо він відтворюється Стан відтворення очищено Використовувати повністю чорну тему Показувати лише тих виконавців, які безпосередньо зазначені в альбомі (найкраще працює в добре позначених бібліотеках) Увага: Встановлення високих позитивних значень попереднього підсилювача може призвести до спотворення звуку в деяких піснях. - При відтворенні з деталей предмета + При відтворенні з деталей предмета Очистити кеш тегів і повністю перезавантажити музичну бібліотеку (повільніше, але ефективніше) Автоматичне перезавантаження Перезавантажувати бібліотеку при виявленні змін (потрібне постійне сповіщення) @@ -164,10 +164,10 @@ Відстеження змін в музичній бібліотеці… Власна дія для панелі відтворення Регулювання без тегів - Відтворення з показаного елемента + Відтворення з показаного елемента Продовжити перемішування після вибору нової пісні - Відтворити виконавця - Відтворити жанр + Відтворити виконавця + Відтворити жанр Перемотати назад перед відтворенням попередньої пісні Зберегти поточний стан відтворення Пересканувати музику @@ -187,7 +187,7 @@ Перемотайте на початок пісні перед відтворенням попередньої Увімкнути заокруглені кути на додаткових елементах інтерфейсу (потрібно заокруглення обкладинок альбомів) Попередній підсилювач застосовується до наявних налаштувань під час відтворення - Відтворити всі пісні + Відтворити всі пісні Перезавантажити музичну бібліотеку, використовуючи кешовані теги, коли це можливо Скісна риска (/) Плюс (+) @@ -274,7 +274,7 @@ Стан відтворення Налаштуйте звук і поведінку при відтворенні Папки - За спаданням + За спаданням Зображення списку відтворення для %s Список відтворення Списки відтворення @@ -303,4 +303,5 @@ Примусові квадратні обкладинки Пісня Переглянути + Відтворити пісню окремо \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index d75168b77..1b91a7870 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -23,9 +23,9 @@ 正在播放 播放 随机 - 从全部歌曲开始播放 - 从专辑开始播放 - 从艺术家播放 + 从全部歌曲开始播放 + 从专辑开始播放 + 从艺术家播放 播放队列 作为下一首播放 加入播放队列 @@ -65,7 +65,7 @@ 偏好专辑 如果已有专辑正在播放则优先增益专辑 行为 - 从音乐库中选择播放时 + 从音乐库中选择播放时 记住随机模式 播放新曲目时保留随机播放模式 切换上一曲前先倒带 @@ -162,9 +162,9 @@ 已加载艺术家数量:%d 已加载流派数量:%d 总计时长:%s - 从展示的项目播放 + 从展示的项目播放 仅从您添加的目录中加载音乐。 - 从项目详情中选择播放时 + 从项目详情中选择播放时 不会从您添加的目录中加载音乐。 高级音乐编码 (AAC) 已加载专辑数量:%d @@ -252,7 +252,7 @@ 随机播放所选 播放所选 选中了 %d 首 - 按流派播放 + 按流派播放 Wiki %1$s, %2$s 重置 @@ -268,7 +268,7 @@ 文件夹 音乐 配置声音和播放行为 - 降序 + 降序 播放列表 播放列表 %s 的播放列表图片 @@ -297,4 +297,5 @@ 将所有专辑封面裁剪至 1:1 宽高比 歌曲 查看 + 自行播放歌曲 \ No newline at end of file diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml index f3c956766..eb07550e3 100644 --- a/app/src/main/res/values/settings.xml +++ b/app/src/main/res/values/settings.xml @@ -27,8 +27,8 @@ auxio_pre_amp_with auxio_pre_amp_without - auxio_library_playback_mode - auxio_detail_playback_mode + auxio_play_in_list_with + auxio_play_in_parent_with KEY_KEEP_SHUFFLE KEY_PREV_REWIND KEY_LOOP_PAUSE @@ -42,7 +42,7 @@ auxio_bar_action auxio_notif_action - KEY_SEARCH_FILTER + KEY_SEARCH_FILTER auxio_songs_sort auxio_albums_sort @@ -106,34 +106,38 @@ @integer/action_mode_shuffle - - @string/set_playback_mode_songs - @string/set_playback_mode_artist - @string/set_playback_mode_album - @string/set_playback_mode_genre + + @string/set_play_song_from_all + @string/set_play_song_from_album + @string/set_play_song_from_artist + @string/set_play_song_from_genre + @string/set_play_song_by_itself - - @integer/music_mode_songs - @integer/music_mode_artist - @integer/music_mode_album - @integer/music_mode_genre + + @integer/play_song_from_all + @integer/play_song_from_album + @integer/play_song_from_artist + @integer/play_song_from_genre + @integer/play_song_by_itself - - @string/set_playback_mode_none - @string/set_playback_mode_songs - @string/set_playback_mode_artist - @string/set_playback_mode_album - @string/set_playback_mode_genre + + @string/set_play_song_none + @string/set_play_song_from_all + @string/set_play_song_from_album + @string/set_play_song_from_artist + @string/set_play_song_from_genre + @string/set_play_song_by_itself - - @integer/music_mode_none - @integer/music_mode_songs - @integer/music_mode_artist - @integer/music_mode_album - @integer/music_mode_genre + + @integer/play_song_none + @integer/play_song_from_all + @integer/play_song_from_album + @integer/play_song_from_artist + @integer/play_song_from_genre + @integer/play_song_by_itself @@ -152,11 +156,12 @@ 1 2 - -2147483648 - 0xA108 - 0xA109 - 0xA10A - 0xA10B + -2147483648 + 0xA11F + 0xA120 + 0xA121 + 0xA122 + 0xA124 0xA111 0xA112 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d610c1984..f2bedcee8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -104,8 +104,10 @@ Date added Sort + Sort by + Direction Ascending - Descending + Descending Now playing Equalizer @@ -208,13 +210,14 @@ Skip to next Repeat mode Behavior - When playing from the library - When playing from item details - Play from shown item - Play from all songs - Play from album - Play from artist - Play from genre + When playing from the library + When playing from item details + Play from shown item + Play from all songs + Play from album + Play from artist + Play from genre + Play song by itself Remember shuffle Keep shuffle on when playing a new song diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml index 0f5675812..98228e1aa 100644 --- a/app/src/main/res/values/styles_ui.xml +++ b/app/src/main/res/values/styles_ui.xml @@ -50,7 +50,13 @@ + +