diff --git a/.github/ISSUE_TEMPLATE/bug-crash-report.yml b/.github/ISSUE_TEMPLATE/bug-crash-report.yml index d34ce4da9..652dba0b8 100644 --- a/.github/ISSUE_TEMPLATE/bug-crash-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-crash-report.yml @@ -59,19 +59,9 @@ body: - type: textarea id: logs attributes: - label: Relevant log output + label: Bug report description: | - If possible, provide a stack trace or a Logcat. This can help identify the issue. - To take a logcat, you must do the following: - 1. Use a desktop/laptop to download the android platform tools from [here](https://developer.android.com/studio/releases/platform-tools). - 2. Extract the downloaded file to a folder. - 3. Enable USB debugging on your phone [Instructions](https://developer.android.com/studio/command-line/adb#Enabling), and then connect your - phone to a laptop. You will get a prompt to "Allow USB debugging" when you run the logcat command. Accept this. - 4. Open up a terminal/command prompt in that folder and run: - - `./adb -d logcat | grep -i "[DWE] Auxio"` in the case of a bug (may require some changes on windows) - - `./adb -d logcat AndroidRuntime:E *:S` in the case of a crash - 5. Copy and paste the output to this area of the issue. - render: shell + If possible, provide a "bug report" ZIP file to make it easier to identify the issue. Go to [here](https://developer.android.com/studio/debug/bug-report) for guidance on how to take one. validations: required: true - type: checkboxes diff --git a/CHANGELOG.md b/CHANGELOG.md index 11e2cbbfe..0201041b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## 3.0.4 + +#### What's New +- Added support for `COMPILATION` and `ITUNESCOMPILATION` flags. + +#### What's Improved +- Accept `REPLAYGAIN_*` adjustment information on OPUS files alongside +`R128_*` adjustments +- List updates are now consistent across the app +- Fixed jarring header update in detail view +- Search view now trims search queries +- Audio effect (equalizer) session is now broadcast when playing/pausing +rather than on start/stop +- Searching now ignores punctuation +- Numeric names are now logically sorted (i.e 7 before 15) + +#### What's Fixed +- Fixed MP4-AAC files not playing due to an accidental audio extractor +deletion +- Fix "format" not appearing in song properties view + +#### What's Changed +- "Ignore articles when sorting" is now "Intelligent sorting" + ## 3.0.3 #### What's New @@ -24,7 +48,6 @@ while selecting it. #### Dev/Meta - Started using dependency injection -- Started code obsfucation - Only bundle audio-related extractors with ExoPlayer - Switched to Room for database management - Updated to MDC 1.8.0 alpha-01 diff --git a/ExoPlayer b/ExoPlayer index 268d683ba..fef2bb3af 160000 --- a/ExoPlayer +++ b/ExoPlayer @@ -1 +1 @@ -Subproject commit 268d683bab060fff43e75732248416d9bf476ef3 +Subproject commit fef2bb3af622f235d98549ffe2efd8f7f7d2aa41 diff --git a/README.md b/README.md index 1b59eb426..373c5680d 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,16 @@

Auxio

A simple, rational music player for android.

- - Latest Version + + Latest Version - Releases + Releases - + - Minimum SDK Version + Minimum SDK Version

Changelog | Wiki

@@ -70,9 +70,10 @@ precise/original dates, sort tags, and more Auxio relies on a custom version of ExoPlayer that enables some extra features. This adds some caveats to the build process: -1. The project uses submodules, so when cloning initially, use `git clone --recurse-submodules` to properly -download in the external code. -2. You are **unable** to build this project on windows, as the custom ExoPlayer build runs shell scripts that +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 will only work on unix-based systems. ## Contributing diff --git a/app/NOTICE b/app/NOTICE index dc9b86ca7..db155ab3c 100644 --- a/app/NOTICE +++ b/app/NOTICE @@ -1,5 +1,6 @@ /* * Copyright (c) $today.year Auxio Project + * $FILE 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 diff --git a/app/build.gradle b/app/build.gradle index 15c5a46d7..69e5a79a7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,8 +20,8 @@ android { defaultConfig { applicationId namespace - versionName "3.0.3" - versionCode 27 + versionName "3.0.4" + versionCode 28 minSdk 21 targetSdk 33 @@ -57,6 +57,14 @@ android { } } + packagingOptions { + exclude "DebugProbesKt.bin" + exclude "kotlin-tooling-metadata.json" + exclude "**/kotlin/**" + exclude "**/okhttp3/**" + exclude "META-INF/*.version" + } + buildFeatures { viewBinding true } @@ -67,7 +75,8 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.4" // --- SUPPORT --- @@ -79,13 +88,13 @@ dependencies { implementation "androidx.fragment:fragment-ktx:1.5.5" // UI - implementation "androidx.recyclerview:recyclerview:1.2.1" + implementation "androidx.recyclerview:recyclerview:1.3.0" implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.viewpager2:viewpager2:1.1.0-beta01" - implementation 'androidx.core:core-ktx:+' + implementation 'androidx.core:core-ktx:1.9.0' // Lifecycle - def lifecycle_version = "2.5.1" + def lifecycle_version = "2.6.0" implementation "androidx.lifecycle:lifecycle-common:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" @@ -128,6 +137,7 @@ dependencies { kapt "com.google.dagger:dagger-compiler:$dagger_version" implementation "com.google.dagger:hilt-android:$hilt_version" kapt "com.google.dagger:hilt-android-compiler:$hilt_version" + // Testing debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' testImplementation "junit:junit:4.13.2" diff --git a/app/src/androidTest/java/org/oxycblt/auxio/StubTest.kt b/app/src/androidTest/java/org/oxycblt/auxio/StubTest.kt index 7de0b5199..32c443adb 100644 --- a/app/src/androidTest/java/org/oxycblt/auxio/StubTest.kt +++ b/app/src/androidTest/java/org/oxycblt/auxio/StubTest.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * StubTest.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 diff --git a/app/src/main/java/org/oxycblt/auxio/Auxio.kt b/app/src/main/java/org/oxycblt/auxio/Auxio.kt index 01f4eecab..77eed0ff9 100644 --- a/app/src/main/java/org/oxycblt/auxio/Auxio.kt +++ b/app/src/main/java/org/oxycblt/auxio/Auxio.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * Auxio.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 @@ -30,6 +31,7 @@ import org.oxycblt.auxio.ui.UISettings /** * A simple, rational music player for android. + * * @author Alexander Capehart (OxygenCobalt) */ @HiltAndroidApp diff --git a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt index 3c724786a..d2d1e933f 100644 --- a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt +++ b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * IntegerTable.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 @@ -20,6 +21,7 @@ package org.oxycblt.auxio /** * A table containing all of the magic integer codes that the codebase has currently reserved. May * be non-contiguous. + * * @author Alexander Capehart (OxygenCobalt) */ object IntegerTable { @@ -35,18 +37,12 @@ object IntegerTable { const val VIEW_TYPE_BASIC_HEADER = 0xA004 /** SortHeaderViewHolder */ const val VIEW_TYPE_SORT_HEADER = 0xA005 - /** AlbumDetailViewHolder */ - const val VIEW_TYPE_ALBUM_DETAIL = 0xA006 /** AlbumSongViewHolder */ const val VIEW_TYPE_ALBUM_SONG = 0xA007 - /** ArtistDetailViewHolder */ - const val VIEW_TYPE_ARTIST_DETAIL = 0xA008 /** ArtistAlbumViewHolder */ const val VIEW_TYPE_ARTIST_ALBUM = 0xA009 /** ArtistSongViewHolder */ const val VIEW_TYPE_ARTIST_SONG = 0xA00A - /** GenreDetailViewHolder */ - const val VIEW_TYPE_GENRE_DETAIL = 0xA00B /** DiscHeaderViewHolder */ const val VIEW_TYPE_DISC_HEADER = 0xA00C /** "Music playback" notification code */ diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index d956bb4ce..1fb9733f2 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * MainActivity.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 @@ -40,17 +41,13 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat /** * Auxio's single [AppCompatActivity]. * - * TODO: Add error screens - * - * TODO: Custom language support - * - * TODO: Use proper material attributes (Not the weird dimen attributes I currently have) - * - * TODO: Migrate to material animation system - * - * TODO: Unit testing - * * @author Alexander Capehart (OxygenCobalt) + * + * TODO: Add error screens + * TODO: Custom language support + * TODO: Use proper material attributes (Not the weird dimen attributes I currently have) + * TODO: Migrate to material animation system + * TODO: Unit testing */ @AndroidEntryPoint class MainActivity : AppCompatActivity() { @@ -112,9 +109,10 @@ class MainActivity : AppCompatActivity() { /** * Transform an [Intent] given to [MainActivity] into a [InternalPlayer.Action] that can be used * in the playback system. + * * @param intent The (new) [Intent] given to this [MainActivity], or null if there is no intent. * @return true If the analogous [InternalPlayer.Action] to the given [Intent] was started, - * false otherwise. + * false otherwise. */ private fun startIntentAction(intent: Intent?): Boolean { if (intent == null) { diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index b036dd393..6c1913d5f 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * MainFragment.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 @@ -51,6 +52,7 @@ import org.oxycblt.auxio.util.* /** * A wrapper around the home fragment that shows the playback fragment and controls the more * high-level navigation features. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint @@ -127,12 +129,12 @@ class MainFragment : } // --- VIEWMODEL SETUP --- - collect(navModel.mainNavigationAction, ::handleMainNavigation) - collect(navModel.exploreNavigationItem, ::handleExploreNavigation) - collect(navModel.exploreArtistNavigationItem, ::handleArtistNavigationPicker) + collect(navModel.mainNavigationAction.flow, ::handleMainNavigation) + collect(navModel.exploreNavigationItem.flow, ::handleExploreNavigation) + collect(navModel.exploreArtistNavigationItem.flow, ::handleArtistNavigationPicker) collectImmediately(playbackModel.song, ::updateSong) - collect(playbackModel.artistPickerSong, ::handlePlaybackArtistPicker) - collect(playbackModel.genrePickerSong, ::handlePlaybackGenrePicker) + collect(playbackModel.artistPickerSong.flow, ::handlePlaybackArtistPicker) + collect(playbackModel.genrePickerSong.flow, ::handlePlaybackGenrePicker) } override fun onStart() { @@ -268,10 +270,11 @@ class MainFragment : when (action) { is MainNavigationAction.Expand -> tryExpandSheets() is MainNavigationAction.Collapse -> tryCollapseSheets() - is MainNavigationAction.Directions -> findNavController().navigate(action.directions) + is MainNavigationAction.Directions -> + findNavController().navigateSafe(action.directions) } - navModel.finishMainNavigation() + navModel.mainNavigationAction.consume() } private fun handleExploreNavigation(item: Music?) { @@ -285,7 +288,7 @@ class MainFragment : navModel.mainNavigateTo( MainNavigationAction.Directions( MainFragmentDirections.actionPickNavigationArtist(item.uid))) - navModel.finishExploreNavigation() + navModel.exploreArtistNavigationItem.consume() } } @@ -302,7 +305,7 @@ class MainFragment : navModel.mainNavigateTo( MainNavigationAction.Directions( MainFragmentDirections.actionPickPlaybackArtist(song.uid))) - playbackModel.finishPlaybackArtistPicker() + playbackModel.artistPickerSong.consume() } } @@ -311,7 +314,7 @@ class MainFragment : navModel.mainNavigateTo( MainNavigationAction.Directions( MainFragmentDirections.actionPickPlaybackGenre(song.uid))) - playbackModel.finishPlaybackGenrePicker() + playbackModel.genrePickerSong.consume() } } 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 74a7eb6bd..12f7098cf 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * AlbumDetailFragment.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 @@ -24,16 +25,18 @@ import android.view.View import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearSmoothScroller import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding -import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter +import org.oxycblt.auxio.detail.header.AlbumDetailHeaderAdapter +import org.oxycblt.auxio.detail.list.AlbumDetailListAdapter +import org.oxycblt.auxio.detail.list.DetailListAdapter import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.Sort -import org.oxycblt.auxio.list.adapter.BasicListInstructions import org.oxycblt.auxio.list.selection.SelectionViewModel import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist @@ -47,11 +50,14 @@ import org.oxycblt.auxio.util.* /** * A [ListFragment] that shows information about an [Album]. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint class AlbumDetailFragment : - ListFragment(), AlbumDetailAdapter.Listener { + ListFragment(), + AlbumDetailHeaderAdapter.Listener, + DetailListAdapter.Listener { private val detailModel: DetailViewModel by activityViewModels() override val navModel: NavigationViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() @@ -59,7 +65,8 @@ class AlbumDetailFragment : // 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() - private val detailAdapter = AlbumDetailAdapter(this) + private val albumHeaderAdapter = AlbumDetailHeaderAdapter(this) + private val albumListAdapter = AlbumDetailListAdapter(this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -86,7 +93,7 @@ class AlbumDetailFragment : setOnMenuItemClickListener(this@AlbumDetailFragment) } - binding.detailRecycler.adapter = detailAdapter + binding.detailRecycler.adapter = ConcatAdapter(albumHeaderAdapter, albumListAdapter) // -- VIEWMODEL SETUP --- // DetailViewModel handles most initialization from the navigation argument. @@ -95,7 +102,7 @@ class AlbumDetailFragment : collectImmediately(detailModel.albumList, ::updateList) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) - collect(navModel.exploreNavigationItem, ::handleNavigation) + collect(navModel.exploreNavigationItem.flow, ::handleNavigation) collectImmediately(selectionModel.selected, ::updateSelection) } @@ -103,6 +110,9 @@ class AlbumDetailFragment : super.onDestroyBinding(binding) binding.detailToolbar.setOnMenuItemClickListener(null) 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 { @@ -181,14 +191,15 @@ class AlbumDetailFragment : return } requireBinding().detailToolbar.title = album.resolveName(requireContext()) + albumHeaderAdapter.setParent(album) } private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) { - detailAdapter.setPlaying(song, isPlaying) + albumListAdapter.setPlaying(song, isPlaying) } else { // Clear the ViewHolders if the mode isn't ALL_SONGS - detailAdapter.setPlaying(null, isPlaying) + albumListAdapter.setPlaying(null, isPlaying) } } @@ -201,11 +212,11 @@ class AlbumDetailFragment : if (unlikelyToBeNull(detailModel.currentAlbum.value) == item.album) { logD("Navigating to a song in this album") scrollToAlbumSong(item) - navModel.finishExploreNavigation() + navModel.exploreNavigationItem.consume() } else { logD("Navigating to another album") findNavController() - .navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.album.uid)) + .navigateSafe(AlbumDetailFragmentDirections.actionShowAlbum(item.album.uid)) } } @@ -215,11 +226,11 @@ class AlbumDetailFragment : if (unlikelyToBeNull(detailModel.currentAlbum.value) == item) { logD("Navigating to the top of this album") binding.detailRecycler.scrollToPosition(0) - navModel.finishExploreNavigation() + navModel.exploreNavigationItem.consume() } else { logD("Navigating to another album") findNavController() - .navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.uid)) + .navigateSafe(AlbumDetailFragmentDirections.actionShowAlbum(item.uid)) } } @@ -227,7 +238,7 @@ class AlbumDetailFragment : is Artist -> { logD("Navigating to another artist") findNavController() - .navigate(AlbumDetailFragmentDirections.actionShowArtist(item.uid)) + .navigateSafe(AlbumDetailFragmentDirections.actionShowArtist(item.uid)) } null -> {} else -> error("Unexpected datatype: ${item::class.java}") @@ -272,12 +283,12 @@ class AlbumDetailFragment : } } - private fun updateList(items: List) { - detailAdapter.submitList(items, BasicListInstructions.DIFF) + private fun updateList(list: List) { + albumListAdapter.update(list, detailModel.albumInstructions.consume()) } private fun updateSelection(selected: List) { - detailAdapter.setSelected(selected.toSet()) + albumListAdapter.setSelected(selected.toSet()) requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size) } } 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 8bdca12ab..eecd5b5bc 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * ArtistDetailFragment.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 @@ -24,16 +25,18 @@ import android.view.View import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.ConcatAdapter import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding -import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter -import org.oxycblt.auxio.detail.recycler.DetailAdapter +import org.oxycblt.auxio.detail.header.ArtistDetailHeaderAdapter +import org.oxycblt.auxio.detail.header.DetailHeaderAdapter +import org.oxycblt.auxio.detail.list.ArtistDetailListAdapter +import org.oxycblt.auxio.detail.list.DetailListAdapter import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.Sort -import org.oxycblt.auxio.list.adapter.BasicListInstructions import org.oxycblt.auxio.list.selection.SelectionViewModel import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist @@ -42,19 +45,18 @@ import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.NavigationViewModel -import org.oxycblt.auxio.util.collect -import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.showToast -import org.oxycblt.auxio.util.unlikelyToBeNull +import org.oxycblt.auxio.util.* /** * A [ListFragment] that shows information about an [Artist]. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint class ArtistDetailFragment : - ListFragment(), DetailAdapter.Listener { + ListFragment(), + DetailHeaderAdapter.Listener, + DetailListAdapter.Listener { private val detailModel: DetailViewModel by activityViewModels() override val navModel: NavigationViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() @@ -62,7 +64,8 @@ class ArtistDetailFragment : // Information about what artist to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an artist. private val args: ArtistDetailFragmentArgs by navArgs() - private val detailAdapter = ArtistDetailAdapter(this) + private val artistHeaderAdapter = ArtistDetailHeaderAdapter(this) + private val artistListAdapter = ArtistDetailListAdapter(this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -89,7 +92,7 @@ class ArtistDetailFragment : setOnMenuItemClickListener(this@ArtistDetailFragment) } - binding.detailRecycler.adapter = detailAdapter + binding.detailRecycler.adapter = ConcatAdapter(artistHeaderAdapter, artistListAdapter) // --- VIEWMODEL SETUP --- // DetailViewModel handles most initialization from the navigation argument. @@ -98,7 +101,7 @@ class ArtistDetailFragment : collectImmediately(detailModel.artistList, ::updateList) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) - collect(navModel.exploreNavigationItem, ::handleNavigation) + collect(navModel.exploreNavigationItem.flow, ::handleNavigation) collectImmediately(selectionModel.selected, ::updateSelection) } @@ -106,6 +109,9 @@ class ArtistDetailFragment : super.onDestroyBinding(binding) binding.detailToolbar.setOnMenuItemClickListener(null) 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 { @@ -194,8 +200,8 @@ class ArtistDetailFragment : findNavController().navigateUp() return } - requireBinding().detailToolbar.title = artist.resolveName(requireContext()) + artistHeaderAdapter.setParent(artist) } private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { @@ -210,7 +216,7 @@ class ArtistDetailFragment : else -> null } - detailAdapter.setPlaying(playingItem, isPlaying) + artistListAdapter.setPlaying(playingItem, isPlaying) } private fun handleNavigation(item: Music?) { @@ -221,14 +227,14 @@ class ArtistDetailFragment : is Song -> { logD("Navigating to another album") findNavController() - .navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.album.uid)) + .navigateSafe(ArtistDetailFragmentDirections.actionShowAlbum(item.album.uid)) } // Launch a new detail view for an album, even if it is part of // this artist. is Album -> { logD("Navigating to another album") findNavController() - .navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.uid)) + .navigateSafe(ArtistDetailFragmentDirections.actionShowAlbum(item.uid)) } // If the artist that should be navigated to is this artist, then // scroll back to the top. Otherwise launch a new detail view. @@ -236,11 +242,11 @@ class ArtistDetailFragment : if (item.uid == detailModel.currentArtist.value?.uid) { logD("Navigating to the top of this artist") binding.detailRecycler.scrollToPosition(0) - navModel.finishExploreNavigation() + navModel.exploreNavigationItem.consume() } else { logD("Navigating to another artist") findNavController() - .navigate(ArtistDetailFragmentDirections.actionShowArtist(item.uid)) + .navigateSafe(ArtistDetailFragmentDirections.actionShowArtist(item.uid)) } } null -> {} @@ -248,12 +254,12 @@ class ArtistDetailFragment : } } - private fun updateList(items: List) { - detailAdapter.submitList(items, BasicListInstructions.DIFF) + private fun updateList(list: List) { + artistListAdapter.update(list, detailModel.artistInstructions.consume()) } private fun updateSelection(selected: List) { - detailAdapter.setSelected(selected.toSet()) + artistListAdapter.setSelected(selected.toSet()) requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size) } } 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 7f3d9f773..d486fa109 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * DetailAppBarLayout.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 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 7c738a768..20923abac 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * DetailViewModel.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 @@ -29,10 +30,11 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.yield import org.oxycblt.auxio.R -import org.oxycblt.auxio.detail.recycler.SortHeader +import org.oxycblt.auxio.detail.list.SortHeader import org.oxycblt.auxio.list.BasicHeader import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Sort +import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.metadata.AudioInfo import org.oxycblt.auxio.music.metadata.Disc @@ -44,6 +46,7 @@ import org.oxycblt.auxio.util.* /** * [ViewModel] that manages the Song, Album, Artist, and Genre detail views. Keeps track of the * current item they are showing, sub-data to display, and configuration. + * * @author Alexander Capehart (OxygenCobalt) */ @HiltViewModel @@ -79,6 +82,10 @@ constructor( /** 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 /** The current [Sort] used for [Song]s in [albumList]. */ var albumSongSort: Sort @@ -86,7 +93,7 @@ constructor( set(value) { musicSettings.albumSongSort = value // Refresh the album list to reflect the new sort. - currentAlbum.value?.let(::refreshAlbumList) + currentAlbum.value?.let { refreshAlbumList(it, true) } } // --- ARTIST --- @@ -99,6 +106,10 @@ constructor( private val _artistList = 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 /** The current [Sort] used for [Song]s in [artistList]. */ var artistSongSort: Sort @@ -106,7 +117,7 @@ constructor( set(value) { musicSettings.artistSongSort = value // Refresh the artist list to reflect the new sort. - currentArtist.value?.let(::refreshArtistList) + currentArtist.value?.let { refreshArtistList(it, true) } } // --- GENRE --- @@ -119,6 +130,10 @@ constructor( private val _genreList = 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 /** The current [Sort] used for [Song]s in [genreList]. */ var genreSongSort: Sort @@ -126,7 +141,7 @@ constructor( set(value) { musicSettings.genreSongSort = value // Refresh the genre list to reflect the new sort. - currentGenre.value?.let(::refreshGenreList) + currentGenre.value?.let { refreshGenreList(it, true) } } /** @@ -182,6 +197,7 @@ constructor( /** * Set a new [currentSong] from it's [Music.UID]. If the [Music.UID] differs, [currentSong] and * [songAudioInfo] will be updated to align with the new [Song]. + * * @param uid The UID of the [Song] to load. Must be valid. */ fun setSongUid(uid: Music.UID) { @@ -196,6 +212,7 @@ constructor( /** * Set a new [currentAlbum] from it's [Music.UID]. If the [Music.UID] differs, [currentAlbum] * and [albumList] will be updated to align with the new [Album]. + * * @param uid The [Music.UID] of the [Album] to update [currentAlbum] to. Must be valid. */ fun setAlbumUid(uid: Music.UID) { @@ -210,6 +227,7 @@ constructor( /** * Set a new [currentArtist] from it's [Music.UID]. If the [Music.UID] differs, [currentArtist] * and [artistList] will be updated to align with the new [Artist]. + * * @param uid The [Music.UID] of the [Artist] to update [currentArtist] to. Must be valid. */ fun setArtistUid(uid: Music.UID) { @@ -224,6 +242,7 @@ constructor( /** * Set a new [currentGenre] from it's [Music.UID]. If the [Music.UID] differs, [currentGenre] * and [genreList] will be updated to align with the new album. + * * @param uid The [Music.UID] of the [Genre] to update [currentGenre] to. Must be valid. */ fun setGenreUid(uid: Music.UID) { @@ -237,10 +256,6 @@ constructor( private fun requireMusic(uid: Music.UID) = musicRepository.library?.find(uid) - /** - * Start a new job to load a given [Song]'s [AudioInfo]. Result is pushed to [songAudioInfo]. - * @param song The song to load. - */ private fun refreshAudioInfo(song: Song) { // Clear any previous job in order to avoid stale data from appearing in the UI. currentSongJob?.cancel() @@ -253,10 +268,17 @@ constructor( } } - private fun refreshAlbumList(album: Album) { + private fun refreshAlbumList(album: Album, replace: Boolean = false) { logD("Refreshing album data") - val data = mutableListOf(album) - data.add(SortHeader(R.string.lbl_songs)) + val list = mutableListOf() + list.add(SortHeader(R.string.lbl_songs)) + val instructions = + if (replace) { + // Intentional so that the header item isn't replaced with the songs + UpdateInstructions.Replace(list.size) + } else { + UpdateInstructions.Diff + } // To create a good user experience regarding disc numbers, we group the album's // songs up by disc and then delimit the groups by a disc header. @@ -266,20 +288,21 @@ constructor( if (byDisc.size > 1) { logD("Album has more than one disc, interspersing headers") for (entry in byDisc.entries) { - data.add(entry.key) - data.addAll(entry.value) + list.add(entry.key) + list.addAll(entry.value) } } else { // Album only has one disc, don't add any redundant headers - data.addAll(songs) + list.addAll(songs) } - _albumList.value = data + _albumInstructions.put(instructions) + _albumList.value = list } - private fun refreshArtistList(artist: Artist) { + private fun refreshArtistList(artist: Artist, replace: Boolean = false) { logD("Refreshing artist data") - val data = mutableListOf(artist) + val list = mutableListOf() val albums = Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING).albums(artist.albums) val byReleaseGroup = @@ -306,35 +329,50 @@ constructor( logD("Release groups for this artist: ${byReleaseGroup.keys}") for (entry in byReleaseGroup.entries.sortedBy { it.key }) { - data.add(BasicHeader(entry.key.headerTitleRes)) - data.addAll(entry.value) + list.add(BasicHeader(entry.key.headerTitleRes)) + list.addAll(entry.value) } // Artists may not be linked to any songs, only include a header entry if we have any. + var instructions: UpdateInstructions = UpdateInstructions.Diff if (artist.songs.isNotEmpty()) { logD("Songs present in this artist, adding header") - data.add(SortHeader(R.string.lbl_songs)) - data.addAll(artistSongSort.songs(artist.songs)) + list.add(SortHeader(R.string.lbl_songs)) + if (replace) { + // Intentional so that the header item isn't replaced with the songs + instructions = UpdateInstructions.Replace(list.size) + } + list.addAll(artistSongSort.songs(artist.songs)) } - _artistList.value = data.toList() + _artistInstructions.put(instructions) + _artistList.value = list.toList() } - private fun refreshGenreList(genre: Genre) { + private fun refreshGenreList(genre: Genre, replace: Boolean = false) { logD("Refreshing genre data") - val data = mutableListOf(genre) + val list = mutableListOf() // Genre is guaranteed to always have artists and songs. - data.add(BasicHeader(R.string.lbl_artists)) - data.addAll(genre.artists) - data.add(SortHeader(R.string.lbl_songs)) - data.addAll(genreSongSort.songs(genre.songs)) - _genreList.value = data + list.add(BasicHeader(R.string.lbl_artists)) + list.addAll(genre.artists) + list.add(SortHeader(R.string.lbl_songs)) + val instructions = + if (replace) { + // Intentional so that the header item isn't replaced with the songs + UpdateInstructions.Replace(list.size) + } else { + UpdateInstructions.Diff + } + list.addAll(genreSongSort.songs(genre.songs)) + _genreInstructions.put(instructions) + _genreList.value = list } /** * A simpler mapping of [ReleaseType] used for grouping and sorting songs. + * * @param headerTitleRes The title string resource to use for a header created out of an - * instance of this enum. + * instance of this enum. */ private enum class AlbumGrouping(@StringRes val headerTitleRes: Int) { ALBUMS(R.string.lbl_albums), 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 ce9fa505c..555d8549a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * GenreDetailFragment.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 @@ -24,16 +25,18 @@ import android.view.View import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.ConcatAdapter import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding -import org.oxycblt.auxio.detail.recycler.DetailAdapter -import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter +import org.oxycblt.auxio.detail.header.DetailHeaderAdapter +import org.oxycblt.auxio.detail.header.GenreDetailHeaderAdapter +import org.oxycblt.auxio.detail.list.DetailListAdapter +import org.oxycblt.auxio.detail.list.GenreDetailListAdapter import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.Sort -import org.oxycblt.auxio.list.adapter.BasicListInstructions import org.oxycblt.auxio.list.selection.SelectionViewModel import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist @@ -43,19 +46,18 @@ import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.NavigationViewModel -import org.oxycblt.auxio.util.collect -import org.oxycblt.auxio.util.collectImmediately -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.showToast -import org.oxycblt.auxio.util.unlikelyToBeNull +import org.oxycblt.auxio.util.* /** * A [ListFragment] that shows information for a particular [Genre]. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint class GenreDetailFragment : - ListFragment(), DetailAdapter.Listener { + ListFragment(), + DetailHeaderAdapter.Listener, + DetailListAdapter.Listener { private val detailModel: DetailViewModel by activityViewModels() override val navModel: NavigationViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() @@ -63,7 +65,8 @@ class GenreDetailFragment : // Information about what genre to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an genre. private val args: GenreDetailFragmentArgs by navArgs() - private val detailAdapter = GenreDetailAdapter(this) + private val genreHeaderAdapter = GenreDetailHeaderAdapter(this) + private val genreListAdapter = GenreDetailListAdapter(this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -88,7 +91,7 @@ class GenreDetailFragment : setOnMenuItemClickListener(this@GenreDetailFragment) } - binding.detailRecycler.adapter = detailAdapter + binding.detailRecycler.adapter = ConcatAdapter(genreHeaderAdapter, genreListAdapter) // --- VIEWMODEL SETUP --- // DetailViewModel handles most initialization from the navigation argument. @@ -97,7 +100,7 @@ class GenreDetailFragment : collectImmediately(detailModel.genreList, ::updateList) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) - collect(navModel.exploreNavigationItem, ::handleNavigation) + collect(navModel.exploreNavigationItem.flow, ::handleNavigation) collectImmediately(selectionModel.selected, ::updateSelection) } @@ -105,6 +108,9 @@ class GenreDetailFragment : super.onDestroyBinding(binding) binding.detailToolbar.setOnMenuItemClickListener(null) 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 { @@ -191,8 +197,8 @@ class GenreDetailFragment : findNavController().navigateUp() return } - requireBinding().detailToolbar.title = genre.resolveName(requireContext()) + genreHeaderAdapter.setParent(genre) } private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { @@ -204,7 +210,7 @@ class GenreDetailFragment : if (parent is Genre && parent.uid == unlikelyToBeNull(detailModel.currentGenre.value).uid) { playingMusic = song } - detailAdapter.setPlaying(playingMusic, isPlaying) + genreListAdapter.setPlaying(playingMusic, isPlaying) } private fun handleNavigation(item: Music?) { @@ -212,31 +218,31 @@ class GenreDetailFragment : is Song -> { logD("Navigating to another song") findNavController() - .navigate(GenreDetailFragmentDirections.actionShowAlbum(item.album.uid)) + .navigateSafe(GenreDetailFragmentDirections.actionShowAlbum(item.album.uid)) } is Album -> { logD("Navigating to another album") findNavController() - .navigate(GenreDetailFragmentDirections.actionShowAlbum(item.uid)) + .navigateSafe(GenreDetailFragmentDirections.actionShowAlbum(item.uid)) } is Artist -> { logD("Navigating to another artist") findNavController() - .navigate(GenreDetailFragmentDirections.actionShowArtist(item.uid)) + .navigateSafe(GenreDetailFragmentDirections.actionShowArtist(item.uid)) } is Genre -> { - navModel.finishExploreNavigation() + navModel.exploreNavigationItem.consume() } null -> {} } } - private fun updateList(items: List) { - detailAdapter.submitList(items, BasicListInstructions.DIFF) + private fun updateList(list: List) { + genreListAdapter.update(list, detailModel.genreInstructions.consume()) } private fun updateSelection(selected: List) { - detailAdapter.setSelected(selected.toSet()) + genreListAdapter.setSelected(selected.toSet()) requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size) } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt b/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt index f0e0924e0..f03ad5c31 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * ReadOnlyTextInput.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 diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt index 1ebf9ff46..337759103 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * SongDetailDialog.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 @@ -28,9 +29,9 @@ import androidx.navigation.fragment.navArgs import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogSongDetailBinding -import org.oxycblt.auxio.detail.recycler.SongProperty -import org.oxycblt.auxio.detail.recycler.SongPropertyAdapter -import org.oxycblt.auxio.list.adapter.BasicListInstructions +import org.oxycblt.auxio.detail.list.SongProperty +import org.oxycblt.auxio.detail.list.SongPropertyAdapter +import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.metadata.AudioInfo @@ -42,6 +43,7 @@ import org.oxycblt.auxio.util.concatLocalized /** * A [ViewBindingDialogFragment] that shows information about a Song. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint @@ -77,7 +79,7 @@ class SongDetailDialog : ViewBindingDialogFragment() { if (info != null) { val context = requireContext() - detailAdapter.submitList( + detailAdapter.update( buildList { add(SongProperty(R.string.lbl_name, song.zipName(context))) add(SongProperty(R.string.lbl_album, song.album.zipName(context))) @@ -102,7 +104,7 @@ class SongDetailDialog : ViewBindingDialogFragment() { SongProperty( R.string.lbl_relative_path, song.path.parent.resolveName(context))) info.resolvedMimeType.resolveName(context)?.let { - SongProperty(R.string.lbl_format, it) + add(SongProperty(R.string.lbl_format, it)) } add( SongProperty( @@ -117,7 +119,7 @@ class SongDetailDialog : ViewBindingDialogFragment() { R.string.lbl_sample_rate, getString(R.string.fmt_sample_rate, it))) } }, - BasicListInstructions.REPLACE) + UpdateInstructions.Replace(0)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/AlbumDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/AlbumDetailHeaderAdapter.kt new file mode 100644 index 000000000..07a552c55 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/AlbumDetailHeaderAdapter.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2023 Auxio Project + * AlbumDetailHeaderAdapter.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.header + +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.oxycblt.auxio.R +import org.oxycblt.auxio.databinding.ItemDetailHeaderBinding +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.resolveNames +import org.oxycblt.auxio.playback.formatDurationMs +import org.oxycblt.auxio.util.context +import org.oxycblt.auxio.util.getPlural +import org.oxycblt.auxio.util.inflater + +/** + * A [DetailHeaderAdapter] that shows [Album] information. + * + * @author Alexander Capehart (OxygenCobalt) + */ +class AlbumDetailHeaderAdapter(private val listener: Listener) : + DetailHeaderAdapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + AlbumDetailHeaderViewHolder.from(parent) + + override fun onBindHeader(holder: AlbumDetailHeaderViewHolder, parent: Album) = + holder.bind(parent, listener) + + /** An extended listener for [DetailHeaderAdapter] implementations. */ + interface Listener : DetailHeaderAdapter.Listener { + + /** + * Called when the artist name in the [Album] header was clicked, requesting navigation to + * it's parent artist. + */ + fun onNavigateToParentArtist() + } +} + +/** + * A [RecyclerView.ViewHolder] that displays the [Album] header in the detail view. Use [from] to + * create an instance. + * + * @author Alexander Capehart (OxygenCobalt) + */ +class AlbumDetailHeaderViewHolder +private constructor(private val binding: ItemDetailHeaderBinding) : + RecyclerView.ViewHolder(binding.root) { + + /** + * Bind new data to this instance. + * + * @param album The new [Album] to bind. + * @param listener A [AlbumDetailHeaderAdapter.Listener] to bind interactions to. + */ + fun bind(album: Album, listener: AlbumDetailHeaderAdapter.Listener) { + binding.detailCover.bind(album) + + // The type text depends on the release type (Album, EP, Single, etc.) + binding.detailType.text = binding.context.getString(album.releaseType.stringRes) + + binding.detailName.text = album.resolveName(binding.context) + + // Artist name maps to the subhead text + binding.detailSubhead.apply { + text = album.artists.resolveNames(context) + + // Add a QoL behavior where navigation to the artist will occur if the artist + // name is pressed. + setOnClickListener { listener.onNavigateToParentArtist() } + } + + // Date, song count, and duration map to the info text + binding.detailInfo.apply { + // Fall back to a friendlier "No date" text if the album doesn't have date information + val date = album.dates?.resolveDate(context) ?: context.getString(R.string.def_date) + val songCount = context.getPlural(R.plurals.fmt_song_count, album.songs.size) + val duration = album.durationMs.formatDurationMs(true) + text = context.getString(R.string.fmt_three, date, songCount, duration) + } + + binding.detailPlayButton.setOnClickListener { listener.onPlay() } + binding.detailShuffleButton.setOnClickListener { listener.onShuffle() } + } + + companion object { + /** + * Create a new instance. + * + * @param parent The parent to inflate this instance from. + * @return A new instance. + */ + fun from(parent: View) = + AlbumDetailHeaderViewHolder(ItemDetailHeaderBinding.inflate(parent.context.inflater)) + } +} 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 new file mode 100644 index 000000000..1268f7caf --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2023 Auxio Project + * ArtistDetailHeaderAdapter.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.header + +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import org.oxycblt.auxio.R +import org.oxycblt.auxio.databinding.ItemDetailHeaderBinding +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.resolveNames +import org.oxycblt.auxio.util.context +import org.oxycblt.auxio.util.getPlural +import org.oxycblt.auxio.util.inflater + +/** + * A [DetailHeaderAdapter] that shows [Artist] information. + * + * @author Alexander Capehart (OxygenCobalt) + */ +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) +} + +/** + * A [RecyclerView.ViewHolder] that displays the [Artist] header in the detail view. Use [from] to + * create an instance. + * + * @author Alexander Capehart (OxygenCobalt) + */ +class ArtistDetailHeaderViewHolder +private constructor(private val binding: ItemDetailHeaderBinding) : + RecyclerView.ViewHolder(binding.root) { + + /** + * Bind new data to this instance. + * + * @param artist The new [Artist] to bind. + * @param listener A [DetailHeaderAdapter.Listener] to bind interactions to. + */ + fun bind(artist: Artist, listener: DetailHeaderAdapter.Listener) { + binding.detailCover.bind(artist) + binding.detailType.text = binding.context.getString(R.string.lbl_artist) + binding.detailName.text = artist.resolveName(binding.context) + + if (artist.songs.isNotEmpty()) { + // Information about the artist's genre(s) map to the sub-head text + binding.detailSubhead.apply { + isVisible = true + text = artist.genres.resolveNames(context) + } + + // Song and album counts map to the info + binding.detailInfo.text = + binding.context.getString( + R.string.fmt_two, + binding.context.getPlural(R.plurals.fmt_album_count, artist.albums.size), + binding.context.getPlural(R.plurals.fmt_song_count, artist.songs.size)) + + // In the case that this header used to he configured to have no songs, + // we want to reset the visibility of all information that was hidden. + binding.detailPlayButton.isVisible = true + binding.detailShuffleButton.isVisible = true + } else { + // The artist does not have any songs, so hide functionality that makes no sense. + // ex. Play and Shuffle, Song Counts, and Genre Information. + // Artists are always guaranteed to have albums however, so continue to show those. + binding.detailSubhead.isVisible = false + binding.detailInfo.text = + binding.context.getPlural(R.plurals.fmt_album_count, artist.albums.size) + binding.detailPlayButton.isVisible = false + binding.detailShuffleButton.isVisible = false + } + + binding.detailPlayButton.setOnClickListener { listener.onPlay() } + binding.detailShuffleButton.setOnClickListener { listener.onShuffle() } + } + + companion object { + /** + * Create a new instance. + * + * @param parent The parent to inflate this instance from. + * @return A new instance. + */ + fun from(parent: View) = + ArtistDetailHeaderViewHolder(ItemDetailHeaderBinding.inflate(parent.context.inflater)) + } +} 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 new file mode 100644 index 000000000..541ed30d9 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 Auxio Project + * DetailHeaderAdapter.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.header + +import androidx.recyclerview.widget.RecyclerView +import org.oxycblt.auxio.music.MusicParent + +/** + * A [RecyclerView.Adapter] that implements shared behavior between each parent header view. + * + * @author Alexander Capehart (OxygenCobalt) + */ +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)) + + /** + * Bind the created header [RecyclerView.ViewHolder] with the current [parent]. + * + * @param holder The [RecyclerView.ViewHolder] to bind. + * @param parent The current [MusicParent] to bind. + */ + abstract fun onBindHeader(holder: VH, parent: T) + + /** + * Update the [MusicParent] shown in the header. + * + * @param parent The new [MusicParent] to show. + */ + fun setParent(parent: T) { + currentParent = parent + notifyItemChanged(0, PAYLOAD_UPDATE_HEADER) + } + + /** An extended listener for [DetailHeaderAdapter] implementations. */ + interface Listener { + /** + * Called when the play button in a detail header is pressed, requesting that the current + * item should be played. + */ + fun onPlay() + + /** + * Called when the shuffle button in a detail header is pressed, requesting that the current + * item should be shuffled + */ + fun onShuffle() + } + + private companion object { + val PAYLOAD_UPDATE_HEADER = Any() + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/GenreDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/GenreDetailHeaderAdapter.kt new file mode 100644 index 000000000..23e4ca855 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/GenreDetailHeaderAdapter.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2023 Auxio Project + * GenreDetailHeaderAdapter.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.header + +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import org.oxycblt.auxio.R +import org.oxycblt.auxio.databinding.ItemDetailHeaderBinding +import org.oxycblt.auxio.detail.list.DetailListAdapter +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.util.context +import org.oxycblt.auxio.util.getPlural +import org.oxycblt.auxio.util.inflater + +/** + * A [DetailHeaderAdapter] that shows [Genre] information. + * + * @author Alexander Capehart (OxygenCobalt) + */ +class GenreDetailHeaderAdapter(private val listener: Listener) : + DetailHeaderAdapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + GenreDetailHeaderViewHolder.from(parent) + + override fun onBindHeader(holder: GenreDetailHeaderViewHolder, parent: Genre) = + holder.bind(parent, listener) +} + +/** + * A [RecyclerView.ViewHolder] that displays the [Genre] header in the detail view. Use [from] to + * create an instance. + * + * @author Alexander Capehart (OxygenCobalt) + */ +class GenreDetailHeaderViewHolder +private constructor(private val binding: ItemDetailHeaderBinding) : + RecyclerView.ViewHolder(binding.root) { + /** + * Bind new data to this instance. + * + * @param genre The new [Genre] to bind. + * @param listener A [DetailListAdapter.Listener] to bind interactions to. + */ + fun bind(genre: Genre, listener: DetailHeaderAdapter.Listener) { + binding.detailCover.bind(genre) + binding.detailType.text = binding.context.getString(R.string.lbl_genre) + binding.detailName.text = genre.resolveName(binding.context) + // Nothing about a genre is applicable to the sub-head text. + binding.detailSubhead.isVisible = false + // The song count of the genre maps to the info text. + binding.detailInfo.text = + binding.context.getString( + R.string.fmt_two, + binding.context.getPlural(R.plurals.fmt_artist_count, genre.artists.size), + binding.context.getPlural(R.plurals.fmt_song_count, genre.songs.size)) + binding.detailPlayButton.setOnClickListener { listener.onPlay() } + binding.detailShuffleButton.setOnClickListener { listener.onShuffle() } + } + + companion object { + /** + * Create a new instance. + * + * @param parent The parent to inflate this instance from. + * @return A new instance. + */ + fun from(parent: View) = + GenreDetailHeaderViewHolder(ItemDetailHeaderBinding.inflate(parent.context.inflater)) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt similarity index 63% rename from app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt index aa573a4e0..84c8683ad 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/AlbumDetailListAdapter.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * AlbumDetailListAdapter.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 @@ -15,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.detail.recycler +package org.oxycblt.auxio.detail.list import android.view.View import android.view.ViewGroup @@ -25,7 +26,6 @@ import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemAlbumSongBinding -import org.oxycblt.auxio.databinding.ItemDetailBinding import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.SelectableListListener @@ -33,36 +33,22 @@ import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.areRawNamesTheSame import org.oxycblt.auxio.music.metadata.Disc -import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.util.context -import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.inflater /** - * An [DetailAdapter] implementing the header and sub-items for the [Album] detail view. - * @param listener A [Listener] to bind interactions to. + * An [DetailListAdapter] implementing the header and sub-items for the [Album] detail view. + * + * @param listener A [DetailListAdapter.Listener] to bind interactions to. * @author Alexander Capehart (OxygenCobalt) */ -class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) { - /** - * An extension to [DetailAdapter.Listener] that enables interactions specific to the album - * detail view. - */ - interface Listener : DetailAdapter.Listener { - /** - * Called when the artist name in the [Album] header was clicked, requesting navigation to - * it's parent artist. - */ - fun onNavigateToParentArtist() - } - +class AlbumDetailListAdapter(private val listener: Listener) : + DetailListAdapter(listener, DIFF_CALLBACK) { override fun getItemViewType(position: Int) = when (getItem(position)) { - // Support the Album header, sub-headers for each disc, and special album songs. - is Album -> AlbumDetailViewHolder.VIEW_TYPE + // Support sub-headers for each disc, and special album songs. is Disc -> DiscViewHolder.VIEW_TYPE is Song -> AlbumSongViewHolder.VIEW_TYPE else -> super.getItemViewType(position) @@ -70,7 +56,6 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { - AlbumDetailViewHolder.VIEW_TYPE -> AlbumDetailViewHolder.from(parent) DiscViewHolder.VIEW_TYPE -> DiscViewHolder.from(parent) AlbumSongViewHolder.VIEW_TYPE -> AlbumSongViewHolder.from(parent) else -> super.onCreateViewHolder(parent, viewType) @@ -79,7 +64,6 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { super.onBindViewHolder(holder, position) when (val item = getItem(position)) { - is Album -> (holder as AlbumDetailViewHolder).bind(item, listener) is Disc -> (holder as DiscViewHolder).bind(item) is Song -> (holder as AlbumSongViewHolder).bind(item, listener) } @@ -98,88 +82,16 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = object : SimpleDiffCallback() { - override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { - return when { - oldItem is Album && newItem is Album -> - AlbumDetailViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) + override fun areContentsTheSame(oldItem: Item, newItem: Item) = + when { oldItem is Disc && newItem is Disc -> DiscViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) oldItem is Song && newItem is Song -> AlbumSongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) // Fall back to DetailAdapter's differ to handle other headers. - else -> DetailAdapter.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) + else -> DetailListAdapter.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) } - } - } - } -} - -/** - * A [RecyclerView.ViewHolder] that displays the [Album] header in the detail view. Use [from] to - * create an instance. - * @author Alexander Capehart (OxygenCobalt) - */ -private class AlbumDetailViewHolder private constructor(private val binding: ItemDetailBinding) : - RecyclerView.ViewHolder(binding.root) { - - /** - * Bind new data to this instance. - * @param album The new [Album] to bind. - * @param listener A [AlbumDetailAdapter.Listener] to bind interactions to. - */ - fun bind(album: Album, listener: AlbumDetailAdapter.Listener) { - binding.detailCover.bind(album) - - // The type text depends on the release type (Album, EP, Single, etc.) - binding.detailType.text = binding.context.getString(album.releaseType.stringRes) - - binding.detailName.text = album.resolveName(binding.context) - - // Artist name maps to the subhead text - binding.detailSubhead.apply { - text = album.artists.resolveNames(context) - - // Add a QoL behavior where navigation to the artist will occur if the artist - // name is pressed. - setOnClickListener { listener.onNavigateToParentArtist() } - } - - // Date, song count, and duration map to the info text - binding.detailInfo.apply { - // Fall back to a friendlier "No date" text if the album doesn't have date information - val date = album.dates?.resolveDate(context) ?: context.getString(R.string.def_date) - val songCount = context.getPlural(R.plurals.fmt_song_count, album.songs.size) - val duration = album.durationMs.formatDurationMs(true) - text = context.getString(R.string.fmt_three, date, songCount, duration) - } - - binding.detailPlayButton.setOnClickListener { listener.onPlay() } - binding.detailShuffleButton.setOnClickListener { listener.onShuffle() } - } - - companion object { - /** A unique ID for this [RecyclerView.ViewHolder] type. */ - const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ALBUM_DETAIL - - /** - * Create a new instance. - * @param parent The parent to inflate this instance from. - * @return A new instance. - */ - fun from(parent: View) = - AlbumDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater)) - - /** A comparator that can be used with DiffUtil. */ - val DIFF_CALLBACK = - object : SimpleDiffCallback() { - override fun areContentsTheSame(oldItem: Album, newItem: Album) = - oldItem.rawName == newItem.rawName && - oldItem.artists.areRawNamesTheSame(newItem.artists) && - oldItem.dates == newItem.dates && - oldItem.songs.size == newItem.songs.size && - oldItem.durationMs == newItem.durationMs && - oldItem.releaseType == newItem.releaseType } } } @@ -187,12 +99,14 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite /** * A [RecyclerView.ViewHolder] that displays a [Disc] to delimit different disc groups. Use [from] * to create an instance. + * * @author Alexander Capehart (OxygenCobalt) */ private class DiscViewHolder(private val binding: ItemDiscHeaderBinding) : RecyclerView.ViewHolder(binding.root) { /** * Bind new data to this instance. + * * @param disc The new [disc] to bind. */ fun bind(disc: Disc) { @@ -209,6 +123,7 @@ private class DiscViewHolder(private val binding: ItemDiscHeaderBinding) : /** * Create a new instance. + * * @param parent The parent to inflate this instance from. * @return A new instance. */ @@ -227,12 +142,14 @@ private class DiscViewHolder(private val binding: ItemDiscHeaderBinding) : /** * A [RecyclerView.ViewHolder] that displays a [Song] in the context of an [Album]. Use [from] to * create an instance. + * * @author Alexander Capehart (OxygenCobalt) */ private class AlbumSongViewHolder private constructor(private val binding: ItemAlbumSongBinding) : SelectionIndicatorAdapter.ViewHolder(binding.root) { /** * Bind new data to this instance. + * * @param song The new [Song] to bind. * @param listener A [SelectableListListener] to bind interactions to. */ @@ -276,6 +193,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA /** * Create a new instance. + * * @param parent The parent to inflate this instance from. * @return A new instance. */ diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/ArtistDetailListAdapter.kt similarity index 61% rename from app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/detail/list/ArtistDetailListAdapter.kt index 655577638..c23c7c20c 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/ArtistDetailListAdapter.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * ArtistDetailListAdapter.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 @@ -15,15 +16,13 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.detail.recycler +package org.oxycblt.auxio.detail.list import android.view.View import android.view.ViewGroup -import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.ItemDetailBinding import org.oxycblt.auxio.databinding.ItemParentBinding import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.list.Item @@ -32,20 +31,19 @@ import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.music.* import org.oxycblt.auxio.util.context -import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.inflater /** - * A [DetailAdapter] implementing the header and sub-items for the [Artist] detail view. - * @param listener A [DetailAdapter.Listener] to bind interactions to. + * A [DetailListAdapter] implementing the header and sub-items for the [Artist] detail view. + * + * @param listener A [DetailListAdapter.Listener] to bind interactions to. * @author Alexander Capehart (OxygenCobalt) */ -class ArtistDetailAdapter(private val listener: Listener) : - DetailAdapter(listener, DIFF_CALLBACK) { +class ArtistDetailListAdapter(private val listener: Listener) : + DetailListAdapter(listener, DIFF_CALLBACK) { override fun getItemViewType(position: Int) = when (getItem(position)) { - // Support an artist header, and special artist albums/songs. - is Artist -> ArtistDetailViewHolder.VIEW_TYPE + // Support a special artist albums/songs. is Album -> ArtistAlbumViewHolder.VIEW_TYPE is Song -> ArtistSongViewHolder.VIEW_TYPE else -> super.getItemViewType(position) @@ -53,7 +51,6 @@ class ArtistDetailAdapter(private val listener: Listener) : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { - ArtistDetailViewHolder.VIEW_TYPE -> ArtistDetailViewHolder.from(parent) ArtistAlbumViewHolder.VIEW_TYPE -> ArtistAlbumViewHolder.from(parent) ArtistSongViewHolder.VIEW_TYPE -> ArtistSongViewHolder.from(parent) else -> super.onCreateViewHolder(parent, viewType) @@ -63,7 +60,6 @@ class ArtistDetailAdapter(private val listener: Listener) : super.onBindViewHolder(holder, position) // Re-binding an item with new data and not just a changed selection/playing state. when (val item = getItem(position)) { - is Artist -> (holder as ArtistDetailViewHolder).bind(item, listener) is Album -> (holder as ArtistAlbumViewHolder).bind(item, listener) is Song -> (holder as ArtistSongViewHolder).bind(item, listener) } @@ -81,93 +77,14 @@ class ArtistDetailAdapter(private val listener: Listener) : /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = object : SimpleDiffCallback() { - override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { - return when { - oldItem is Artist && newItem is Artist -> - ArtistDetailViewHolder.DIFF_CALLBACK.areContentsTheSame( - oldItem, newItem) + override fun areContentsTheSame(oldItem: Item, newItem: Item) = + when { oldItem is Album && newItem is Album -> ArtistAlbumViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) oldItem is Song && newItem is Song -> ArtistSongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) - else -> DetailAdapter.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) + else -> DetailListAdapter.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) } - } - } - } -} - -/** - * A [RecyclerView.ViewHolder] that displays the [Artist] header in the detail view. Use [from] to - * create an instance. - * @author Alexander Capehart (OxygenCobalt) - */ -private class ArtistDetailViewHolder private constructor(private val binding: ItemDetailBinding) : - RecyclerView.ViewHolder(binding.root) { - - /** - * Bind new data to this instance. - * @param artist The new [Artist] to bind. - * @param listener A [DetailAdapter.Listener] to bind interactions to. - */ - fun bind(artist: Artist, listener: DetailAdapter.Listener<*>) { - binding.detailCover.bind(artist) - binding.detailType.text = binding.context.getString(R.string.lbl_artist) - binding.detailName.text = artist.resolveName(binding.context) - - if (artist.songs.isNotEmpty()) { - // Information about the artist's genre(s) map to the sub-head text - binding.detailSubhead.apply { - isVisible = true - text = artist.genres.resolveNames(context) - } - - // Song and album counts map to the info - binding.detailInfo.text = - binding.context.getString( - R.string.fmt_two, - binding.context.getPlural(R.plurals.fmt_album_count, artist.albums.size), - binding.context.getPlural(R.plurals.fmt_song_count, artist.songs.size)) - - // In the case that this header used to he configured to have no songs, - // we want to reset the visibility of all information that was hidden. - binding.detailPlayButton.isVisible = true - binding.detailShuffleButton.isVisible = true - } else { - // The artist does not have any songs, so hide functionality that makes no sense. - // ex. Play and Shuffle, Song Counts, and Genre Information. - // Artists are always guaranteed to have albums however, so continue to show those. - binding.detailSubhead.isVisible = false - binding.detailInfo.text = - binding.context.getPlural(R.plurals.fmt_album_count, artist.albums.size) - binding.detailPlayButton.isVisible = false - binding.detailShuffleButton.isVisible = false - } - - binding.detailPlayButton.setOnClickListener { listener.onPlay() } - binding.detailShuffleButton.setOnClickListener { listener.onShuffle() } - } - - companion object { - /** A unique ID for this [RecyclerView.ViewHolder] type. */ - const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_DETAIL - - /** - * Create a new instance. - * @param parent The parent to inflate this instance from. - * @return A new instance. - */ - fun from(parent: View) = - ArtistDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater)) - - /** A comparator that can be used with DiffUtil. */ - val DIFF_CALLBACK = - object : SimpleDiffCallback() { - override fun areContentsTheSame(oldItem: Artist, newItem: Artist) = - oldItem.rawName == newItem.rawName && - oldItem.genres.areRawNamesTheSame(newItem.genres) && - oldItem.albums.size == newItem.albums.size && - oldItem.songs.size == newItem.songs.size } } } @@ -175,12 +92,14 @@ private class ArtistDetailViewHolder private constructor(private val binding: It /** * A [RecyclerView.ViewHolder] that displays an [Album] in the context of an [Artist]. Use [from] to * create an instance. + * * @author Alexander Capehart (OxygenCobalt) */ private class ArtistAlbumViewHolder private constructor(private val binding: ItemParentBinding) : SelectionIndicatorAdapter.ViewHolder(binding.root) { /** * Bind new data to this instance. + * * @param album The new [Album] to bind. * @param listener An [SelectableListListener] to bind interactions to. */ @@ -209,6 +128,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite /** * Create a new instance. + * * @param parent The parent to inflate this instance from. * @return A new instance. */ @@ -227,12 +147,14 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite /** * A [RecyclerView.ViewHolder] that displays a [Song] in the context of an [Artist]. Use [from] to * create an instance. + * * @author Alexander Capehart (OxygenCobalt) */ private class ArtistSongViewHolder private constructor(private val binding: ItemSongBinding) : SelectionIndicatorAdapter.ViewHolder(binding.root) { /** * Bind new data to this instance. + * * @param song The new [Song] to bind. * @param listener An [SelectableListListener] to bind interactions to. */ @@ -258,6 +180,7 @@ private class ArtistSongViewHolder private constructor(private val binding: Item /** * Create a new instance. + * * @param parent The parent to inflate this instance from. * @return A new instance. */ diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt similarity index 83% rename from app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt index a529aa5ac..7959ec47d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * DetailListAdapter.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 @@ -15,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.detail.recycler +package org.oxycblt.auxio.detail.list import android.view.View import android.view.ViewGroup @@ -36,18 +37,18 @@ import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater /** - * A [RecyclerView.Adapter] that implements behavior shared across each detail view's adapters. + * A [RecyclerView.Adapter] that implements shared behavior between lists of child items in the + * detail views. + * * @param listener A [Listener] to bind interactions to. - * @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the - * internal list. + * @param diffCallback A [DiffUtil.ItemCallback] to compare list updates with. * @author Alexander Capehart (OxygenCobalt) */ -abstract class DetailAdapter( +abstract class DetailListAdapter( private val listener: Listener<*>, - diffCallback: DiffUtil.ItemCallback + private val diffCallback: DiffUtil.ItemCallback ) : - SelectionIndicatorAdapter( - ListDiffer.Async(diffCallback)), + SelectionIndicatorAdapter(diffCallback), AuxioRecyclerView.SpanSizeLookup { override fun getItemViewType(position: Int) = @@ -78,21 +79,8 @@ abstract class DetailAdapter( return item is BasicHeader || item is SortHeader } - /** An extended [SelectableListListener] for [DetailAdapter] implementations. */ + /** An extended [SelectableListListener] for [DetailListAdapter] implementations. */ interface Listener : SelectableListListener { - // TODO: Split off into sub-listeners if a collapsing toolbar is implemented. - /** - * Called when the play button in a detail header is pressed, requesting that the current - * item should be played. - */ - fun onPlay() - - /** - * Called when the shuffle button in a detail header is pressed, requesting that the current - * item should be shuffled - */ - fun onShuffle() - /** * Called when the button in a [SortHeader] item is pressed, requesting that the sort menu * should be opened. @@ -119,6 +107,7 @@ abstract class DetailAdapter( /** * A header variation that displays a button to open a sort menu. + * * @param titleRes The string resource to use as the header title * @author Alexander Capehart (OxygenCobalt) */ @@ -127,16 +116,18 @@ data class SortHeader(@StringRes override val titleRes: Int) : Header /** * A [RecyclerView.ViewHolder] that displays a [SortHeader], a variation on [BasicHeader] that adds * a button opening a menu for sorting. Use [from] to create an instance. + * * @author Alexander Capehart (OxygenCobalt) */ private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) : RecyclerView.ViewHolder(binding.root) { /** * Bind new data to this instance. + * * @param sortHeader The new [SortHeader] to bind. - * @param listener An [DetailAdapter.Listener] to bind interactions to. + * @param listener An [DetailListAdapter.Listener] to bind interactions to. */ - fun bind(sortHeader: SortHeader, listener: DetailAdapter.Listener<*>) { + fun bind(sortHeader: SortHeader, listener: DetailListAdapter.Listener<*>) { binding.headerTitle.text = binding.context.getString(sortHeader.titleRes) binding.headerButton.apply { // Add a Tooltip based on the content description so that the purpose of this @@ -152,6 +143,7 @@ private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) : /** * Create a new instance. + * * @param parent The parent to inflate this instance from. * @return A new instance. */ diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/GenreDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/GenreDetailListAdapter.kt new file mode 100644 index 000000000..67ebe3781 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/GenreDetailListAdapter.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2021 Auxio Project + * GenreDetailListAdapter.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.list + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.oxycblt.auxio.list.Item +import org.oxycblt.auxio.list.adapter.SimpleDiffCallback +import org.oxycblt.auxio.list.recycler.ArtistViewHolder +import org.oxycblt.auxio.list.recycler.SongViewHolder +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.Song + +/** + * An [DetailListAdapter] implementing the header and sub-items for the [Genre] detail view. + * + * @param listener A [DetailListAdapter.Listener] to bind interactions to. + * @author Alexander Capehart (OxygenCobalt) + */ +class GenreDetailListAdapter(private val listener: Listener) : + DetailListAdapter(listener, DIFF_CALLBACK) { + override fun getItemViewType(position: Int) = + when (getItem(position)) { + // Support generic Artist/Song items. + is Artist -> ArtistViewHolder.VIEW_TYPE + is Song -> SongViewHolder.VIEW_TYPE + else -> super.getItemViewType(position) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + when (viewType) { + ArtistViewHolder.VIEW_TYPE -> ArtistViewHolder.from(parent) + SongViewHolder.VIEW_TYPE -> SongViewHolder.from(parent) + else -> super.onCreateViewHolder(parent, viewType) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + super.onBindViewHolder(holder, position) + when (val item = getItem(position)) { + is Artist -> (holder as ArtistViewHolder).bind(item, listener) + is Song -> (holder as SongViewHolder).bind(item, listener) + } + } + + override fun isItemFullWidth(position: Int): Boolean { + if (super.isItemFullWidth(position)) { + return true + } + // Genre headers should be full-width in all configurations + return getItem(position) is Genre + } + + private companion object { + val DIFF_CALLBACK = + object : SimpleDiffCallback() { + override fun areContentsTheSame(oldItem: Item, newItem: Item) = + when { + oldItem is Artist && newItem is Artist -> + ArtistViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) + oldItem is Song && newItem is Song -> + SongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) + else -> DetailListAdapter.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) + } + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/SongPropertyAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/SongPropertyAdapter.kt similarity index 87% rename from app/src/main/java/org/oxycblt/auxio/detail/recycler/SongPropertyAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/detail/list/SongPropertyAdapter.kt index 863a921e5..690f3a792 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/SongPropertyAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/SongPropertyAdapter.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * SongPropertyAdapter.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 @@ -15,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.detail.recycler +package org.oxycblt.auxio.detail.list import android.view.View import android.view.ViewGroup @@ -23,21 +24,19 @@ import androidx.annotation.StringRes import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.databinding.ItemSongPropertyBinding import org.oxycblt.auxio.list.Item -import org.oxycblt.auxio.list.adapter.BasicListInstructions -import org.oxycblt.auxio.list.adapter.DiffAdapter -import org.oxycblt.auxio.list.adapter.ListDiffer -import org.oxycblt.auxio.list.adapter.SimpleDiffCallback +import org.oxycblt.auxio.list.adapter.* import org.oxycblt.auxio.list.recycler.DialogRecyclerView import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater /** * An adapter for [SongProperty] instances. + * * @author Alexander Capehart (OxygenCobalt) */ class SongPropertyAdapter : - DiffAdapter( - ListDiffer.Blocking(SongPropertyViewHolder.DIFF_CALLBACK)) { + FlexibleListAdapter( + SongPropertyViewHolder.DIFF_CALLBACK) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = SongPropertyViewHolder.from(parent) @@ -48,6 +47,7 @@ class SongPropertyAdapter : /** * A property entry for use in [SongPropertyAdapter]. + * * @param name The contextual title to use for the property. * @param value The value of the property. * @author Alexander Capehart (OxygenCobalt) @@ -56,6 +56,7 @@ data class SongProperty(@StringRes val name: Int, val value: String) : Item /** * A [RecyclerView.ViewHolder] that displays a [SongProperty]. Use [from] to create an instance. + * * @author Alexander Capehart (OxygenCobalt) */ class SongPropertyViewHolder private constructor(private val binding: ItemSongPropertyBinding) : @@ -69,6 +70,7 @@ class SongPropertyViewHolder private constructor(private val binding: ItemSongPr companion object { /** * Create a new instance. + * * @param parent The parent to inflate this instance from. * @return A new instance. */ diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt deleted file mode 100644 index 51a86f335..000000000 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (c) 2021 Auxio Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.detail.recycler - -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import org.oxycblt.auxio.IntegerTable -import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.ItemDetailBinding -import org.oxycblt.auxio.list.Item -import org.oxycblt.auxio.list.adapter.SimpleDiffCallback -import org.oxycblt.auxio.list.recycler.ArtistViewHolder -import org.oxycblt.auxio.list.recycler.SongViewHolder -import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.music.Genre -import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.util.context -import org.oxycblt.auxio.util.getPlural -import org.oxycblt.auxio.util.inflater - -/** - * An [DetailAdapter] implementing the header and sub-items for the [Genre] detail view. - * @param listener A [DetailAdapter.Listener] to bind interactions to. - * @author Alexander Capehart (OxygenCobalt) - */ -class GenreDetailAdapter(private val listener: Listener) : - DetailAdapter(listener, DIFF_CALLBACK) { - override fun getItemViewType(position: Int) = - when (getItem(position)) { - // Support the Genre header and generic Artist/Song items. There's nothing about - // a genre that will make the artists/songs specially formatted, so it doesn't matter - // what we use for their ViewHolders. - is Genre -> GenreDetailViewHolder.VIEW_TYPE - is Artist -> ArtistViewHolder.VIEW_TYPE - is Song -> SongViewHolder.VIEW_TYPE - else -> super.getItemViewType(position) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - when (viewType) { - GenreDetailViewHolder.VIEW_TYPE -> GenreDetailViewHolder.from(parent) - ArtistViewHolder.VIEW_TYPE -> ArtistViewHolder.from(parent) - SongViewHolder.VIEW_TYPE -> SongViewHolder.from(parent) - else -> super.onCreateViewHolder(parent, viewType) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - super.onBindViewHolder(holder, position) - when (val item = getItem(position)) { - is Genre -> (holder as GenreDetailViewHolder).bind(item, listener) - is Artist -> (holder as ArtistViewHolder).bind(item, listener) - is Song -> (holder as SongViewHolder).bind(item, listener) - } - } - - override fun isItemFullWidth(position: Int): Boolean { - if (super.isItemFullWidth(position)) { - return true - } - // Genre headers should be full-width in all configurations - return getItem(position) is Genre - } - - private companion object { - val DIFF_CALLBACK = - object : SimpleDiffCallback() { - override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { - return when { - oldItem is Genre && newItem is Genre -> - GenreDetailViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) - oldItem is Artist && newItem is Artist -> - ArtistViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) - oldItem is Song && newItem is Song -> - SongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) - else -> DetailAdapter.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) - } - } - } - } -} - -/** - * A [RecyclerView.ViewHolder] that displays the [Genre] header in the detail view. Use [from] to - * create an instance. - * @author Alexander Capehart (OxygenCobalt) - */ -private class GenreDetailViewHolder private constructor(private val binding: ItemDetailBinding) : - RecyclerView.ViewHolder(binding.root) { - /** - * Bind new data to this instance. - * @param genre The new [Song] to bind. - * @param listener A [DetailAdapter.Listener] to bind interactions to. - */ - fun bind(genre: Genre, listener: DetailAdapter.Listener<*>) { - binding.detailCover.bind(genre) - binding.detailType.text = binding.context.getString(R.string.lbl_genre) - binding.detailName.text = genre.resolveName(binding.context) - // Nothing about a genre is applicable to the sub-head text. - binding.detailSubhead.isVisible = false - // The song count of the genre maps to the info text. - binding.detailInfo.text = - binding.context.getString( - R.string.fmt_two, - binding.context.getPlural(R.plurals.fmt_artist_count, genre.artists.size), - binding.context.getPlural(R.plurals.fmt_song_count, genre.songs.size)) - binding.detailPlayButton.setOnClickListener { listener.onPlay() } - binding.detailShuffleButton.setOnClickListener { listener.onShuffle() } - } - - companion object { - /** A unique ID for this [RecyclerView.ViewHolder] type. */ - const val VIEW_TYPE = IntegerTable.VIEW_TYPE_GENRE_DETAIL - - /** - * Create a new instance. - * @param parent The parent to inflate this instance from. - * @return A new instance. - */ - fun from(parent: View) = - GenreDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater)) - - /** A comparator that can be used with DiffUtil. */ - val DIFF_CALLBACK = - object : SimpleDiffCallback() { - override fun areContentsTheSame(oldItem: Genre, newItem: Genre) = - oldItem.rawName == newItem.rawName && - oldItem.songs.size == newItem.songs.size && - oldItem.durationMs == newItem.durationMs - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/home/EdgeFrameLayout.kt b/app/src/main/java/org/oxycblt/auxio/home/EdgeFrameLayout.kt index 81fe40edd..5deec4fcc 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/EdgeFrameLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/EdgeFrameLayout.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * EdgeFrameLayout.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 @@ -27,6 +28,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat /** * A [FrameLayout] that automatically applies bottom insets. + * * @author Alexander Capehart (OxygenCobalt) */ class EdgeFrameLayout 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 41a7f2240..65b9e6f35 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * HomeFragment.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 @@ -64,6 +65,7 @@ import org.oxycblt.auxio.util.* /** * The starting [SelectionFragment] of Auxio. Shows the user's music library and enables navigation * to other views. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint @@ -153,11 +155,11 @@ class HomeFragment : binding.homeFab.setOnClickListener { playbackModel.shuffleAll() } // --- VIEWMODEL SETUP --- - collect(homeModel.shouldRecreate, ::handleRecreate) + collect(homeModel.recreateTabs.flow, ::handleRecreate) collectImmediately(homeModel.currentTabMode, ::updateCurrentTab) collectImmediately(homeModel.songsList, homeModel.isFastScrolling, ::updateFab) collectImmediately(musicModel.indexerState, ::updateIndexerState) - collect(navModel.exploreNavigationItem, ::handleNavigation) + collect(navModel.exploreNavigationItem.flow, ::handleNavigation) collectImmediately(selectionModel.selected, ::updateSelection) } @@ -199,7 +201,7 @@ class HomeFragment : R.id.action_search -> { logD("Navigating to search") setupAxisTransitions(MaterialSharedAxis.Z) - findNavController().navigate(HomeFragmentDirections.actionShowSearch()) + findNavController().navigateSafe(HomeFragmentDirections.actionShowSearch()) } R.id.action_settings -> { logD("Navigating to settings") @@ -328,18 +330,14 @@ class HomeFragment : } } - private fun handleRecreate(recreate: Boolean) { - if (!recreate) { - // Nothing to do - return - } - + private fun handleRecreate(recreate: Unit?) { + if (recreate == null) return val binding = requireBinding() // Move back to position zero, as there must be a tab there. binding.homePager.currentItem = 0 // Make sure tabs are set up to also follow the new ViewPager configuration. setupPager(binding) - homeModel.finishRecreate() + homeModel.recreateTabs.consume() } private fun updateIndexerState(state: Indexer.State?) { @@ -456,7 +454,7 @@ class HomeFragment : } setupAxisTransitions(MaterialSharedAxis.X) - findNavController().navigate(action) + findNavController().navigateSafe(action) } private fun updateSelection(selected: List) { @@ -483,10 +481,11 @@ class HomeFragment : /** * [FragmentStateAdapter] implementation for the [HomeFragment]'s [ViewPager2] instance. + * * @param tabs The current tab configuration. This will define the [Fragment]s created. * @param fragmentManager The [FragmentManager] required by [FragmentStateAdapter]. * @param lifecycleOwner The [LifecycleOwner], whose Lifecycle is required by - * [FragmentStateAdapter]. + * [FragmentStateAdapter]. */ private class HomePagerAdapter( private val tabs: List, diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeModule.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeModule.kt index ddff79aa7..a578b6e07 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeModule.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * HomeModule.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 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 2499b5918..776b0b219 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * HomeSettings.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 @@ -28,6 +29,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull /** * User configuration specific to the home UI. + * * @author Alexander Capehart (OxygenCobalt) */ interface HomeSettings : Settings { 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 7fe81ed7f..adcb3473e 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * HomeViewModel.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 @@ -24,13 +25,17 @@ 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.adapter.UpdateInstructions import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.model.Library import org.oxycblt.auxio.playback.PlaybackSettings +import org.oxycblt.auxio.util.Event +import org.oxycblt.auxio.util.MutableEvent import org.oxycblt.auxio.util.logD /** * The ViewModel for managing the tab data and lists of the home view. + * * @author Alexander Capehart (OxygenCobalt) */ @HiltViewModel @@ -47,11 +52,19 @@ constructor( /** 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 private val _albumsLists = 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 private val _artistsList = MutableStateFlow(listOf()) /** @@ -61,11 +74,19 @@ constructor( */ val artistsList: MutableStateFlow> get() = _artistsList + private val _artistsInstructions = MutableEvent() + /** Instructions for how to update [artistsList] in the UI. */ + val artistsInstructions: Event + get() = _artistsInstructions private val _genresList = 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 /** The [MusicMode] to use when playing a [Song] from the UI. */ val playbackMode: MusicMode @@ -82,13 +103,14 @@ constructor( /** The [MusicMode] of the currently shown [Tab]. */ val currentTabMode: StateFlow = _currentTabMode - private val _shouldRecreate = MutableStateFlow(false) + private val _shouldRecreate = MutableEvent() /** * A marker to re-create all library tabs, usually initiated by a settings change. When this * flag is true, all tabs (and their respective ViewPager2 fragments) will be re-created from * scratch. */ - val shouldRecreate: StateFlow = _shouldRecreate + val recreateTabs: Event + get() = _shouldRecreate private val _isFastScrolling = MutableStateFlow(false) /** A marker for whether the user is fast-scrolling in the home view or not. */ @@ -108,10 +130,14 @@ constructor( override fun onLibraryChanged(library: Library?) { if (library != null) { logD("Library changed, refreshing library") + // FIXME: Sort name setting changes result in incorrect list updates // 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(library.songs) + _albumsInstructions.put(UpdateInstructions.Diff) _albumsLists.value = musicSettings.albumSort.albums(library.albums) + _artistsInstructions.put(UpdateInstructions.Diff) _artistsList.value = musicSettings.artistSort.artists( if (homeSettings.shouldHideCollaborators) { @@ -120,6 +146,7 @@ constructor( } else { library.artists }) + _genresInstructions.put(UpdateInstructions.Diff) _genresList.value = musicSettings.genreSort.genres(library.genres) } } @@ -127,7 +154,7 @@ constructor( override fun onTabsChanged() { // Tabs changed, update the current tabs and set up a re-create event. currentTabModes = makeTabModes() - _shouldRecreate.value = true + _shouldRecreate.put(Unit) } override fun onHideCollaboratorsChanged() { @@ -138,6 +165,7 @@ constructor( /** * Get the preferred [Sort] for a given [Tab]. + * * @param tabMode The [MusicMode] of the [Tab] desired. * @return The [Sort] preferred for that [Tab] */ @@ -151,6 +179,7 @@ constructor( /** * 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) { @@ -159,18 +188,22 @@ constructor( when (_currentTabMode.value) { MusicMode.SONGS -> { musicSettings.songSort = sort + _songsInstructions.put(UpdateInstructions.Replace(0)) _songsList.value = sort.songs(_songsList.value) } MusicMode.ALBUMS -> { musicSettings.albumSort = sort + _albumsInstructions.put(UpdateInstructions.Replace(0)) _albumsLists.value = sort.albums(_albumsLists.value) } MusicMode.ARTISTS -> { musicSettings.artistSort = sort + _artistsInstructions.put(UpdateInstructions.Replace(0)) _artistsList.value = sort.artists(_artistsList.value) } MusicMode.GENRES -> { musicSettings.genreSort = sort + _genresInstructions.put(UpdateInstructions.Replace(0)) _genresList.value = sort.genres(_genresList.value) } } @@ -178,6 +211,7 @@ constructor( /** * Update [currentTabMode] to reflect a new ViewPager2 position + * * @param pagerPos The new position of the ViewPager2 instance. */ fun synchronizeTabPosition(pagerPos: Int) { @@ -185,16 +219,9 @@ constructor( _currentTabMode.value = currentTabModes[pagerPos] } - /** - * Mark the recreation process as complete. - * @see shouldRecreate - */ - fun finishRecreate() { - _shouldRecreate.value = false - } - /** * Update whether the user is fast scrolling or not in the home view. + * * @param isFastScrolling true if the user is currently fast scrolling, false otherwise. */ fun setFastScrolling(isFastScrolling: Boolean) { @@ -204,8 +231,9 @@ constructor( /** * Create a list of [MusicMode]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 - * the same way as the configuration. + * the same way as the configuration. */ private fun makeTabModes() = homeSettings.homeTabs.filterIsInstance().map { it.mode } 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 da7cd4554..3a848edf9 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * FastScrollPopupView.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 @@ -40,6 +41,7 @@ import org.oxycblt.auxio.util.isRtl /** * A [MaterialTextView] that displays the popup indicator used in FastScrollRecyclerView + * * @author Alexander Capehart (OxygenCobalt), Hai Zhang */ class FastScrollPopupView diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt index 863d5b32a..1d0cc6737 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * FastScrollRecyclerView.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 @@ -49,7 +50,7 @@ import org.oxycblt.auxio.util.* * * !!! MODIFICATIONS !!!: * - Scroller will no longer show itself on startup or relayouts, which looked unpleasant with - * multiple views + * multiple views * - DefaultAnimationHelper and RecyclerViewHelper were merged into the class * - FastScroller overlay was merged into RecyclerView instance * - Removed FastScrollerBuilder @@ -61,11 +62,10 @@ import org.oxycblt.auxio.util.* * - Added drag listener * - Added documentation * - * TODO: Add vibration when popup changes - * - * TODO: Improve support for variably sized items (Re-back with library fast scroller?) - * * @author Hai Zhang, Alexander Capehart (OxygenCobalt) + * + * TODO: Add vibration when popup changes + * TODO: Improve support for variably sized items (Re-back with library fast scroller?) */ class FastScrollRecyclerView @JvmOverloads @@ -508,9 +508,10 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr interface PopupProvider { /** * Get text to use in the popup at the specified position. + * * @param pos The position in the list. * @return A [String] to use in the popup. Null if there is no applicable text for the popup - * at [pos]. + * at [pos]. */ fun getPopup(pos: Int): String? } @@ -519,6 +520,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr interface Listener { /** * Called when the fast scrolling state changes. + * * @param isFastScrolling true if the user is currently fast scrolling, false otherwise. */ fun onFastScrollingChanged(isFastScrolling: Boolean) 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 8820db820..b5b9135dd 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * AlbumListFragment.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 @@ -32,8 +33,6 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.list.* import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.Sort -import org.oxycblt.auxio.list.adapter.BasicListInstructions -import org.oxycblt.auxio.list.adapter.ListDiffer import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.AlbumViewHolder import org.oxycblt.auxio.list.selection.SelectionViewModel @@ -46,6 +45,7 @@ import org.oxycblt.auxio.util.collectImmediately /** * A [ListFragment] that shows a list of [Album]s. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint @@ -75,7 +75,7 @@ class AlbumListFragment : listener = this@AlbumListFragment } - collectImmediately(homeModel.albumsList, ::updateList) + collectImmediately(homeModel.albumsList, ::updateAlbums) collectImmediately(selectionModel.selected, ::updateSelection) collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) } @@ -94,11 +94,10 @@ class AlbumListFragment : // Change how we display the popup depending on the current sort mode. return when (homeModel.getSortForTab(MusicMode.ALBUMS).mode) { // By Name -> Use Name - is Sort.Mode.ByName -> album.collationKey?.run { sourceString.first().uppercase() } + is Sort.Mode.ByName -> album.sortName?.thumbString // By Artist -> Use name of first artist - is Sort.Mode.ByArtist -> - album.artists[0].collationKey?.run { sourceString.first().uppercase() } + is Sort.Mode.ByArtist -> album.artists[0].sortName?.thumbString // Date -> Use minimum date (Maximum dates are not sorted by, so showing them is odd) is Sort.Mode.ByDate -> album.dates?.run { min.resolveDate(requireContext()) } @@ -139,8 +138,8 @@ class AlbumListFragment : openMusicMenu(anchor, R.menu.menu_album_actions, item) } - private fun updateList(albums: List) { - albumAdapter.submitList(albums, BasicListInstructions.REPLACE) + private fun updateAlbums(albums: List) { + albumAdapter.update(albums, homeModel.albumsInstructions.consume()) } private fun updateSelection(selection: List) { @@ -154,11 +153,11 @@ class AlbumListFragment : /** * A [SelectionIndicatorAdapter] that shows a list of [Album]s using [AlbumViewHolder]. + * * @param listener An [SelectableListListener] to bind interactions to. */ private class AlbumAdapter(private val listener: SelectableListListener) : - SelectionIndicatorAdapter( - ListDiffer.Blocking(AlbumViewHolder.DIFF_CALLBACK)) { + SelectionIndicatorAdapter(AlbumViewHolder.DIFF_CALLBACK) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = AlbumViewHolder.from(parent) 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 5d9ec7357..c6a58f594 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * ArtistListFragment.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 @@ -30,8 +31,6 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.list.* import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.Sort -import org.oxycblt.auxio.list.adapter.BasicListInstructions -import org.oxycblt.auxio.list.adapter.ListDiffer import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.ArtistViewHolder import org.oxycblt.auxio.list.selection.SelectionViewModel @@ -43,10 +42,12 @@ import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.nonZeroOrNull /** * A [ListFragment] that shows a list of [Artist]s. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint @@ -73,7 +74,7 @@ class ArtistListFragment : listener = this@ArtistListFragment } - collectImmediately(homeModel.artistsList, ::updateList) + collectImmediately(homeModel.artistsList, ::updateArtists) collectImmediately(selectionModel.selected, ::updateSelection) collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) } @@ -92,7 +93,7 @@ class ArtistListFragment : // Change how we display the popup depending on the current sort mode. return when (homeModel.getSortForTab(MusicMode.ARTISTS).mode) { // By Name -> Use Name - is Sort.Mode.ByName -> artist.collationKey?.run { sourceString.first().uppercase() } + is Sort.Mode.ByName -> artist.sortName?.thumbString // Duration -> Use formatted duration is Sort.Mode.ByDuration -> artist.durationMs?.formatDurationMs(false) @@ -117,8 +118,8 @@ class ArtistListFragment : openMusicMenu(anchor, R.menu.menu_artist_actions, item) } - private fun updateList(artists: List) { - artistAdapter.submitList(artists, BasicListInstructions.REPLACE) + private fun updateArtists(artists: List) { + artistAdapter.update(artists, homeModel.artistsInstructions.consume().also { logD(it) }) } private fun updateSelection(selection: List) { @@ -132,11 +133,11 @@ class ArtistListFragment : /** * A [SelectionIndicatorAdapter] that shows a list of [Artist]s using [ArtistViewHolder]. + * * @param listener An [SelectableListListener] to bind interactions to. */ private class ArtistAdapter(private val listener: SelectableListListener) : - SelectionIndicatorAdapter( - ListDiffer.Blocking(ArtistViewHolder.DIFF_CALLBACK)) { + SelectionIndicatorAdapter(ArtistViewHolder.DIFF_CALLBACK) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ArtistViewHolder.from(parent) 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 863eb22cb..3561abbb4 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * GenreListFragment.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 @@ -30,8 +31,6 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.list.* import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.Sort -import org.oxycblt.auxio.list.adapter.BasicListInstructions -import org.oxycblt.auxio.list.adapter.ListDiffer import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.GenreViewHolder import org.oxycblt.auxio.list.selection.SelectionViewModel @@ -43,9 +42,11 @@ import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.util.collectImmediately +import org.oxycblt.auxio.util.logD /** * A [ListFragment] that shows a list of [Genre]s. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint @@ -72,7 +73,7 @@ class GenreListFragment : listener = this@GenreListFragment } - collectImmediately(homeModel.genresList, ::updateList) + collectImmediately(homeModel.genresList, ::updateGenres) collectImmediately(selectionModel.selected, ::updateSelection) collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) } @@ -91,7 +92,7 @@ class GenreListFragment : // Change how we display the popup depending on the current sort mode. return when (homeModel.getSortForTab(MusicMode.GENRES).mode) { // By Name -> Use Name - is Sort.Mode.ByName -> genre.collationKey?.run { sourceString.first().uppercase() } + is Sort.Mode.ByName -> genre.sortName?.thumbString // Duration -> Use formatted duration is Sort.Mode.ByDuration -> genre.durationMs.formatDurationMs(false) @@ -116,8 +117,8 @@ class GenreListFragment : openMusicMenu(anchor, R.menu.menu_artist_actions, item) } - private fun updateList(artists: List) { - genreAdapter.submitList(artists, BasicListInstructions.REPLACE) + private fun updateGenres(genres: List) { + genreAdapter.update(genres, homeModel.genresInstructions.consume().also { logD(it) }) } private fun updateSelection(selection: List) { @@ -131,11 +132,11 @@ class GenreListFragment : /** * A [SelectionIndicatorAdapter] that shows a list of [Genre]s using [GenreViewHolder]. + * * @param listener An [SelectableListListener] to bind interactions to. */ private class GenreAdapter(private val listener: SelectableListListener) : - SelectionIndicatorAdapter( - ListDiffer.Blocking(GenreViewHolder.DIFF_CALLBACK)) { + SelectionIndicatorAdapter(GenreViewHolder.DIFF_CALLBACK) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = GenreViewHolder.from(parent) 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 1990737df..9dc512b99 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * SongListFragment.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 @@ -32,8 +33,6 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.list.* import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.Sort -import org.oxycblt.auxio.list.adapter.BasicListInstructions -import org.oxycblt.auxio.list.adapter.ListDiffer import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.SongViewHolder import org.oxycblt.auxio.list.selection.SelectionViewModel @@ -49,6 +48,7 @@ import org.oxycblt.auxio.util.collectImmediately /** * A [ListFragment] that shows a list of [Song]s. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint @@ -78,7 +78,7 @@ class SongListFragment : listener = this@SongListFragment } - collectImmediately(homeModel.songsList, ::updateList) + collectImmediately(homeModel.songsList, ::updateSongs) collectImmediately(selectionModel.selected, ::updateSelection) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) @@ -100,15 +100,13 @@ class SongListFragment : // based off the names of the parent objects and not the child objects. return when (homeModel.getSortForTab(MusicMode.SONGS).mode) { // Name -> Use name - is Sort.Mode.ByName -> song.collationKey?.run { sourceString.first().uppercase() } + is Sort.Mode.ByName -> song.sortName?.thumbString // Artist -> Use name of first artist - is Sort.Mode.ByArtist -> - song.album.artists[0].collationKey?.run { sourceString.first().uppercase() } + is Sort.Mode.ByArtist -> song.album.artists[0].sortName?.thumbString // Album -> Use Album Name - is Sort.Mode.ByAlbum -> - song.album.collationKey?.run { sourceString.first().uppercase() } + is Sort.Mode.ByAlbum -> song.album.sortName?.thumbString // Year -> Use Full Year is Sort.Mode.ByDate -> song.album.dates?.resolveDate(requireContext()) @@ -146,8 +144,8 @@ class SongListFragment : openMusicMenu(anchor, R.menu.menu_song_actions, item) } - private fun updateList(songs: List) { - songAdapter.submitList(songs, BasicListInstructions.REPLACE) + private fun updateSongs(songs: List) { + songAdapter.update(songs, homeModel.songsInstructions.consume()) } private fun updateSelection(selection: List) { @@ -165,11 +163,11 @@ class SongListFragment : /** * A [SelectionIndicatorAdapter] that shows a list of [Song]s using [SongViewHolder]. + * * @param listener An [SelectableListListener] to bind interactions to. */ private class SongAdapter(private val listener: SelectableListListener) : - SelectionIndicatorAdapter( - ListDiffer.Blocking(SongViewHolder.DIFF_CALLBACK)) { + SelectionIndicatorAdapter(SongViewHolder.DIFF_CALLBACK) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = SongViewHolder.from(parent) 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 d1a97ba58..e39c4a90f 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * AdaptiveTabStrategy.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 @@ -27,6 +28,7 @@ import org.oxycblt.auxio.util.logD /** * A [TabLayoutMediator.TabConfigurationStrategy] that uses larger/smaller tab configurations * depending on the screen configuration. + * * @param context [Context] required to obtain window information * @param tabs Current tab configuration from settings * @author Alexander Capehart (OxygenCobalt) 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 76f7cf95d..e4aeb5d57 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * Tab.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 @@ -22,18 +23,21 @@ import org.oxycblt.auxio.util.logE /** * A representation of a library tab suitable for configuration. + * * @param mode The type of list in the home view this instance corresponds to. * @author Alexander Capehart (OxygenCobalt) */ sealed class Tab(open val mode: MusicMode) { /** * 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. */ data class Visible(override val mode: MusicMode) : Tab(mode) /** * 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. */ data class Invisible(override val mode: MusicMode) : Tab(mode) @@ -68,6 +72,7 @@ sealed class Tab(open val mode: MusicMode) { /** * Convert an array of [Tab]s into it's integer representation. + * * @param tabs The array of [Tab]s to convert * @return An integer representation of the [Tab] array */ @@ -93,6 +98,7 @@ sealed class Tab(open val mode: MusicMode) { /** * Convert a [Tab] integer representation into it's corresponding array of [Tab]s. + * * @param intCode The integer representation of the [Tab]s. * @return An array of [Tab]s corresponding to the sequence. */ 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 c60084cf9..de754bba9 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * TabAdapter.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 @@ -30,6 +31,7 @@ import org.oxycblt.auxio.util.inflater /** * A [RecyclerView.Adapter] that displays an array of [Tab]s open for configuration. + * * @param listener A [EditableListListener] for tab interactions. */ class TabAdapter(private val listener: EditableListListener) : @@ -46,6 +48,7 @@ class TabAdapter(private val listener: EditableListListener) : /** * Immediately update the tab array. This should be used when initializing the list. + * * @param newTabs The new array of tabs to show. */ fun submitTabs(newTabs: Array) { @@ -55,6 +58,7 @@ class TabAdapter(private val listener: EditableListListener) : /** * Update a specific tab to the given value. + * * @param at The position of the tab to update. * @param tab The new tab. */ @@ -66,6 +70,7 @@ class TabAdapter(private val listener: EditableListListener) : /** * Swap two tabs with each other. + * * @param a The position of the first tab to swap. * @param b The position of the second tab to swap. */ @@ -83,12 +88,14 @@ class TabAdapter(private val listener: EditableListListener) : /** * A [RecyclerView.ViewHolder] that displays a [Tab]. Use [from] to create an instance. + * * @author Alexander Capehart (OxygenCobalt) */ class TabViewHolder private constructor(private val binding: ItemTabBinding) : DialogRecyclerView.ViewHolder(binding.root) { /** * Bind new data to this instance. + * * @param tab The new [Tab] to bind. * @param listener A [EditableListListener] to bind interactions to. */ @@ -114,6 +121,7 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) : companion object { /** * Create a new instance. + * * @param parent The parent to inflate this instance from. * @return A new instance. */ 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 516a54257..536a205bb 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * TabCustomizeDialog.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 @@ -34,6 +35,7 @@ import org.oxycblt.auxio.util.logD /** * A [ViewBindingDialogFragment] that allows the user to modify the home [Tab] configuration. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt index 0eeb29b1e..064d5f8dd 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * TabDragCallback.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 @@ -23,6 +24,7 @@ import androidx.recyclerview.widget.RecyclerView /** * An [ItemTouchHelper.Callback] that implements dragging in the [TabAdapter]. + * * @author Alexander Capehart (OxygenCobalt) */ class TabDragCallback(private val adapter: TabAdapter) : ItemTouchHelper.Callback() { diff --git a/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt b/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt index 70a4a912b..32bc3cd14 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * BitmapProvider.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 @@ -55,14 +56,16 @@ constructor( interface Target { /** * Configure the [ImageRequest.Builder] to enable [Target]-specific configuration. + * * @param builder The [ImageRequest.Builder] that will be used to request the desired - * [Bitmap]. + * [Bitmap]. * @return The same [ImageRequest.Builder] in order to easily chain configuration methods. */ fun onConfigRequest(builder: ImageRequest.Builder): ImageRequest.Builder = builder /** * Called when the loading process is completed. + * * @param bitmap The loaded bitmap, or null if the bitmap could not be loaded. */ fun onCompleted(bitmap: Bitmap?) @@ -77,6 +80,7 @@ constructor( /** * Load the Album cover [Bitmap] from a [Song]. + * * @param song The song to load a [Bitmap] of it's album cover from. * @param target The [Target] to deliver the [Bitmap] to asynchronously. */ diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverMode.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverMode.kt index d1a656f4c..1daaddc91 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverMode.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * CoverMode.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,6 +22,7 @@ import org.oxycblt.auxio.IntegerTable /** * Represents the options available for album cover loading. + * * @author Alexander Capehart (OxygenCobalt) */ enum class CoverMode { @@ -33,6 +35,7 @@ enum class CoverMode { /** * The integer representation of this instance. + * * @see fromIntCode */ val intCode: Int @@ -46,6 +49,7 @@ enum class CoverMode { companion object { /** * Convert a [CoverMode] integer representation into an instance. + * * @param intCode An integer representation of a [CoverMode] * @return The corresponding [CoverMode], or null if the [CoverMode] is invalid. * @see CoverMode.intCode diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt index 68adab079..550f805e3 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * ImageGroup.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 @@ -48,9 +49,9 @@ import org.oxycblt.auxio.util.getInteger * This class is primarily intended for list items. For other uses, [StyledImageView] is more * suitable. * - * TODO: Rework content descriptions here - * * @author Alexander Capehart (OxygenCobalt) + * + * TODO: Rework content descriptions here */ class ImageGroup @JvmOverloads @@ -146,6 +147,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr /** * Bind a [Song] to the internal [StyledImageView]. + * * @param song The [Song] to bind to the view. * @see StyledImageView.bind */ @@ -153,6 +155,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr /** * Bind a [Album] to the internal [StyledImageView]. + * * @param album The [Album] to bind to the view. * @see StyledImageView.bind */ @@ -160,6 +163,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr /** * Bind a [Genre] to the internal [StyledImageView]. + * * @param artist The [Artist] to bind to the view. * @see StyledImageView.bind */ @@ -167,6 +171,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr /** * Bind a [Genre] to the internal [StyledImageView]. + * * @param genre The [Genre] to bind to the view. * @see StyledImageView.bind */ diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt index 1520abf1e..ac9dd75c9 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * ImageModule.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 @@ -27,11 +28,7 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton -import org.oxycblt.auxio.image.extractor.AlbumCoverFetcher -import org.oxycblt.auxio.image.extractor.ArtistImageFetcher -import org.oxycblt.auxio.image.extractor.ErrorCrossfadeTransitionFactory -import org.oxycblt.auxio.image.extractor.GenreImageFetcher -import org.oxycblt.auxio.image.extractor.MusicKeyer +import org.oxycblt.auxio.image.extractor.* @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt index 866cdca2f..7f1aca57f 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * ImageSettings.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 @@ -27,6 +28,7 @@ import org.oxycblt.auxio.util.logD /** * User configuration specific to image loading. + * * @author Alexander Capehart (OxygenCobalt) */ interface ImageSettings : Settings { diff --git a/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt b/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt index 0c2fe5b98..68c9bcd44 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * PlaybackIndicatorView.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 diff --git a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt index 61e197263..85ea9c730 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * StyledImageView.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 @@ -48,11 +49,10 @@ import org.oxycblt.auxio.util.getDrawableCompat /** * An [AppCompatImageView] with some additional styling, including: - * * - Tonal background * - Rounded corners based on user preferences * - Built-in support for binding image data or using a static icon with the same styling as - * placeholder drawables. + * placeholder drawables. * * @author Alexander Capehart (OxygenCobalt) */ @@ -97,34 +97,39 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr /** * Bind a [Song]'s album cover to this view, also updating the content description. + * * @param song The [Song] to bind. */ fun bind(song: Song) = bindImpl(song, R.drawable.ic_song_24, R.string.desc_album_cover) /** * Bind an [Album]'s cover to this view, also updating the content description. + * * @param album the [Album] to bind. */ fun bind(album: Album) = bindImpl(album, R.drawable.ic_album_24, R.string.desc_album_cover) /** * Bind an [Artist]'s image to this view, also updating the content description. + * * @param artist the [Artist] to bind. */ fun bind(artist: Artist) = bindImpl(artist, R.drawable.ic_artist_24, R.string.desc_artist_image) /** * Bind an [Genre]'s image to this view, also updating the content description. + * * @param genre the [Genre] to bind. */ fun bind(genre: Genre) = bindImpl(genre, R.drawable.ic_genre_24, R.string.desc_genre_image) /** * Internally bind a [Music]'s image to this view. + * * @param music The music to find. * @param errorRes The error drawable resource to use if the music cannot be loaded. * @param descRes The content description string resource to use. The resource must have one - * field for the name of the [Music]. + * field for the name of the [Music]. */ private fun bindImpl(music: Music, @DrawableRes errorRes: Int, @StringRes descRes: Int) { val request = @@ -144,6 +149,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr /** * A [Drawable] wrapper that re-styles the drawable to better align with the style of * [StyledImageView]. + * * @param context [Context] required for initialization. * @param inner The [Drawable] to wrap. */ diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt index 64c9a948a..8c9ff2e56 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * Components.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 @@ -31,7 +32,6 @@ import javax.inject.Inject import kotlin.math.min import okio.buffer import okio.source -import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist @@ -41,6 +41,7 @@ import org.oxycblt.auxio.music.Song /** * A [Keyer] implementation for [Music] data. + * * @author Alexander Capehart (OxygenCobalt) */ class MusicKeyer : Keyer { @@ -56,16 +57,17 @@ class MusicKeyer : Keyer { /** * Generic [Fetcher] for [Album] covers. Works with both [Album] and [Song]. Use [SongFactory] or * [AlbumFactory] for instantiation. + * * @author Alexander Capehart (OxygenCobalt) */ class AlbumCoverFetcher private constructor( private val context: Context, - private val imageSettings: ImageSettings, + private val extractor: CoverExtractor, private val album: Album ) : Fetcher { override suspend fun fetch(): FetchResult? = - Covers.fetch(context, imageSettings, album)?.run { + extractor.extract(album)?.run { SourceResult( source = ImageSource(source().buffer(), context), mimeType = null, @@ -73,77 +75,79 @@ private constructor( } /** A [Fetcher.Factory] implementation that works with [Song]s. */ - class SongFactory @Inject constructor(private val imageSettings: ImageSettings) : + class SongFactory @Inject constructor(private val coverExtractor: CoverExtractor) : Fetcher.Factory { override fun create(data: Song, options: Options, imageLoader: ImageLoader) = - AlbumCoverFetcher(options.context, imageSettings, data.album) + AlbumCoverFetcher(options.context, coverExtractor, data.album) } /** A [Fetcher.Factory] implementation that works with [Album]s. */ - class AlbumFactory @Inject constructor(private val imageSettings: ImageSettings) : + class AlbumFactory @Inject constructor(private val coverExtractor: CoverExtractor) : Fetcher.Factory { override fun create(data: Album, options: Options, imageLoader: ImageLoader) = - AlbumCoverFetcher(options.context, imageSettings, data) + AlbumCoverFetcher(options.context, coverExtractor, data) } } /** * [Fetcher] for [Artist] images. Use [Factory] for instantiation. + * * @author Alexander Capehart (OxygenCobalt) */ class ArtistImageFetcher private constructor( private val context: Context, - private val imageSettings: ImageSettings, + private val extractor: CoverExtractor, private val size: Size, private val artist: Artist ) : Fetcher { override suspend fun fetch(): FetchResult? { // Pick the "most prominent" albums (i.e albums with the most songs) to show in the image. val albums = Sort(Sort.Mode.ByCount, Sort.Direction.DESCENDING).albums(artist.albums) - val results = - albums.mapAtMostNotNull(4) { album -> Covers.fetch(context, imageSettings, album) } + val results = albums.mapAtMostNotNull(4) { album -> extractor.extract(album) } return Images.createMosaic(context, results, size) } /** [Fetcher.Factory] implementation. */ - class Factory @Inject constructor(private val imageSettings: ImageSettings) : + class Factory @Inject constructor(private val extractor: CoverExtractor) : Fetcher.Factory { override fun create(data: Artist, options: Options, imageLoader: ImageLoader) = - ArtistImageFetcher(options.context, imageSettings, options.size, data) + ArtistImageFetcher(options.context, extractor, options.size, data) } } /** * [Fetcher] for [Genre] images. Use [Factory] for instantiation. + * * @author Alexander Capehart (OxygenCobalt) */ class GenreImageFetcher private constructor( private val context: Context, - private val imageSettings: ImageSettings, + private val extractor: CoverExtractor, private val size: Size, private val genre: Genre ) : Fetcher { override suspend fun fetch(): FetchResult? { - val results = genre.albums.mapAtMostNotNull(4) { Covers.fetch(context, imageSettings, it) } + val results = genre.albums.mapAtMostNotNull(4) { album -> extractor.extract(album) } return Images.createMosaic(context, results, size) } /** [Fetcher.Factory] implementation. */ - class Factory @Inject constructor(private val imageSettings: ImageSettings) : + class Factory @Inject constructor(private val extractor: CoverExtractor) : Fetcher.Factory { override fun create(data: Genre, options: Options, imageLoader: ImageLoader) = - GenreImageFetcher(options.context, imageSettings, options.size, data) + GenreImageFetcher(options.context, extractor, options.size, data) } } /** * Map at most N [T] items a collection into a collection of [R], ignoring [T] that cannot be * transformed into [R]. + * * @param n The maximum amount of items to map. * @param transform The function that transforms data [T] from the original list into data [R] in - * the new list. Can return null if the [T] cannot be transformed into an [R]. + * the new list. Can return null if the [T] cannot be transformed into an [R]. * @return A new list of at most N non-null [R] items. */ private inline fun Collection.mapAtMostNotNull( 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 new file mode 100644 index 000000000..1c2bb113d --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2023 Auxio Project + * CoverExtractor.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.image.extractor + +import android.content.Context +import android.media.MediaMetadataRetriever +import com.google.android.exoplayer2.MediaItem +import com.google.android.exoplayer2.MediaMetadata +import com.google.android.exoplayer2.MetadataRetriever +import com.google.android.exoplayer2.metadata.flac.PictureFrame +import com.google.android.exoplayer2.metadata.id3.ApicFrame +import com.google.android.exoplayer2.source.MediaSource +import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.ByteArrayInputStream +import java.io.InputStream +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.guava.asDeferred +import kotlinx.coroutines.withContext +import org.oxycblt.auxio.image.CoverMode +import org.oxycblt.auxio.image.ImageSettings +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.logW + +class CoverExtractor +@Inject +constructor( + @ApplicationContext private val context: Context, + private val imageSettings: ImageSettings, + private val mediaSourceFactory: MediaSource.Factory +) { + + suspend fun extract(album: Album): InputStream? = + try { + when (imageSettings.coverMode) { + CoverMode.OFF -> null + CoverMode.MEDIA_STORE -> extractMediaStoreCover(album) + CoverMode.QUALITY -> extractQualityCover(album) + } + } catch (e: Exception) { + logW("Unable to extract album cover due to an error: $e") + null + } + + private suspend fun extractQualityCover(album: Album) = + extractAospMetadataCover(album) + ?: extractExoplayerCover(album) ?: extractMediaStoreCover(album) + + private fun extractAospMetadataCover(album: Album): InputStream? = + MediaMetadataRetriever().run { + // This call is time-consuming but it also doesn't seem to hold up the main thread, + // so it's probably fine not to wrap it.rmt + setDataSource(context, album.songs[0].uri) + + // Get the embedded picture from MediaMetadataRetriever, which will return a full + // ByteArray of the cover without any compression artifacts. + // If its null [i.e there is no embedded cover], than just ignore it and move on + return embeddedPicture?.let { ByteArrayInputStream(it) }.also { release() } + } + + private suspend fun extractExoplayerCover(album: Album): InputStream? { + val tracks = + MetadataRetriever.retrieveMetadata( + mediaSourceFactory, MediaItem.fromUri(album.songs[0].uri)) + .asDeferred() + .await() + + // The metadata extraction process of ExoPlayer results in a dump of all metadata + // it found, which must be iterated through. + val metadata = tracks[0].getFormat(0).metadata + + if (metadata == null || metadata.length() == 0) { + // No (parsable) metadata. This is also expected. + return null + } + + var stream: ByteArrayInputStream? = null + + for (i in 0 until metadata.length()) { + // We can only extract pictures from two tags with this method, ID3v2's APIC or + // Vorbis picture comments. + val pic: ByteArray? + val type: Int + + when (val entry = metadata.get(i)) { + is ApicFrame -> { + pic = entry.pictureData + type = entry.pictureType + } + is PictureFrame -> { + pic = entry.pictureData + type = entry.pictureType + } + else -> continue + } + + if (type == MediaMetadata.PICTURE_TYPE_FRONT_COVER) { + logD("Front cover found") + stream = ByteArrayInputStream(pic) + break + } else if (stream == null) { + stream = ByteArrayInputStream(pic) + } + } + + return stream + } + + @Suppress("BlockingMethodInNonBlockingContext") + private suspend fun extractMediaStoreCover(album: Album) = + // Eliminate any chance that this blocking call might mess up the loading process + withContext(Dispatchers.IO) { context.contentResolver.openInputStream(album.coverUri) } +} diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Covers.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Covers.kt deleted file mode 100644 index 16b14f1a1..000000000 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Covers.kt +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (c) 2022 Auxio Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.image.extractor - -import android.content.Context -import android.media.MediaMetadataRetriever -import com.google.android.exoplayer2.MediaItem -import com.google.android.exoplayer2.MediaMetadata -import com.google.android.exoplayer2.MetadataRetriever -import com.google.android.exoplayer2.metadata.flac.PictureFrame -import com.google.android.exoplayer2.metadata.id3.ApicFrame -import com.google.android.exoplayer2.source.DefaultMediaSourceFactory -import java.io.ByteArrayInputStream -import java.io.InputStream -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.oxycblt.auxio.image.CoverMode -import org.oxycblt.auxio.image.ImageSettings -import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.AudioOnlyExtractors -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW - -/** - * Internal utilities for loading album covers. - * @author Alexander Capehart (OxygenCobalt). - */ -object Covers { - /** - * Fetch an album cover, respecting the current cover configuration. - * @param context [Context] required to load the image. - * @param imageSettings [ImageSettings] required to obtain configuration information. - * @param album [Album] to load the cover from. - * @return An [InputStream] of image data if the cover loading was successful, null if the cover - * loading failed or should not occur. - */ - suspend fun fetch(context: Context, imageSettings: ImageSettings, album: Album): InputStream? { - return try { - when (imageSettings.coverMode) { - CoverMode.OFF -> null - CoverMode.MEDIA_STORE -> fetchMediaStoreCovers(context, album) - CoverMode.QUALITY -> fetchQualityCovers(context, album) - } - } catch (e: Exception) { - logW("Unable to extract album cover due to an error: $e") - null - } - } - - /** - * Load an [Album] cover directly from one of it's Song files. This attempts the following in - * order: - * - [MediaMetadataRetriever], as it has the best support and speed. - * - ExoPlayer's [MetadataRetriever], as some devices (notably Samsung) can have broken - * [MediaMetadataRetriever] implementations. - * - MediaStore, as a last-ditch fallback if the format is really obscure. - * - * @param context [Context] required to load the image. - * @param album [Album] to load the cover from. - * @return An [InputStream] of image data if the cover loading was successful, null otherwise. - */ - private suspend fun fetchQualityCovers(context: Context, album: Album) = - fetchAospMetadataCovers(context, album) - ?: fetchExoplayerCover(context, album) ?: fetchMediaStoreCovers(context, album) - - /** - * Loads an album cover with [MediaMetadataRetriever]. - * @param context [Context] required to load the image. - * @param album [Album] to load the cover from. - * @return An [InputStream] of image data if the cover loading was successful, null otherwise. - */ - private fun fetchAospMetadataCovers(context: Context, album: Album): InputStream? { - MediaMetadataRetriever().apply { - // This call is time-consuming but it also doesn't seem to hold up the main thread, - // so it's probably fine not to wrap it.rmt - setDataSource(context, album.songs[0].uri) - - // Get the embedded picture from MediaMetadataRetriever, which will return a full - // ByteArray of the cover without any compression artifacts. - // If its null [i.e there is no embedded cover], than just ignore it and move on - return embeddedPicture?.let { ByteArrayInputStream(it) }.also { release() } - } - } - - /** - * Loads an [Album] cover with ExoPlayer's [MetadataRetriever]. - * @param context [Context] required to load the image. - * @param album [Album] to load the cover from. - * @return An [InputStream] of image data if the cover loading was successful, null otherwise. - */ - private suspend fun fetchExoplayerCover(context: Context, album: Album): InputStream? { - val uri = album.songs[0].uri - val future = - MetadataRetriever.retrieveMetadata( - DefaultMediaSourceFactory(context, AudioOnlyExtractors), MediaItem.fromUri(uri)) - - // future.get is a blocking call that makes us spin until the future is done. - // This is bad for a co-routine, as it prevents cancellation and by extension - // messes with the image loading process and causes annoying bugs. - // To fix this we wrap this around in a withContext call to make it suspend and make - // sure that the runner can do other coroutines. - @Suppress("BlockingMethodInNonBlockingContext") - val tracks = - withContext(Dispatchers.Default) { - try { - future.get() - } catch (e: Exception) { - null - } - } - - if (tracks == null || tracks.isEmpty) { - // Unrecognized format. This is expected, as ExoPlayer only supports a - // subset of formats. - return null - } - - // The metadata extraction process of ExoPlayer results in a dump of all metadata - // it found, which must be iterated through. - val metadata = tracks[0].getFormat(0).metadata - - if (metadata == null || metadata.length() == 0) { - // No (parsable) metadata. This is also expected. - return null - } - - var stream: ByteArrayInputStream? = null - - for (i in 0 until metadata.length()) { - // We can only extract pictures from two tags with this method, ID3v2's APIC or - // Vorbis picture comments. - val pic: ByteArray? - val type: Int - - when (val entry = metadata.get(i)) { - is ApicFrame -> { - pic = entry.pictureData - type = entry.pictureType - } - is PictureFrame -> { - pic = entry.pictureData - type = entry.pictureType - } - else -> continue - } - - if (type == MediaMetadata.PICTURE_TYPE_FRONT_COVER) { - logD("Front cover found") - stream = ByteArrayInputStream(pic) - break - } else if (stream == null) { - stream = ByteArrayInputStream(pic) - } - } - - return stream - } - - /** - * Loads an [Album] cover from MediaStore. - * @param context [Context] required to load the image. - * @param album [Album] to load the cover from. - * @return An [InputStream] of image data if the cover loading was successful, null otherwise. - */ - @Suppress("BlockingMethodInNonBlockingContext") - private suspend fun fetchMediaStoreCovers(context: Context, album: Album): InputStream? { - // Eliminate any chance that this blocking call might mess up the loading process - return withContext(Dispatchers.IO) { - context.contentResolver.openInputStream(album.coverUri) - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/ErrorCrossfadeTransitionFactory.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/ErrorCrossfadeTransitionFactory.kt index 676cc53bb..7ba7b43e3 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/ErrorCrossfadeTransitionFactory.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/ErrorCrossfadeTransitionFactory.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * ErrorCrossfadeTransitionFactory.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 @@ -27,6 +28,7 @@ import coil.transition.TransitionTarget /** * A copy of [CrossfadeTransition.Factory] that also applies a transition to error results. + * * @author Coil Team, Alexander Capehart (OxygenCobalt) */ class ErrorCrossfadeTransitionFactory : Transition.Factory { diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Images.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Images.kt index df9f4ba19..9be96132b 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Images.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Images.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * Images.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 @@ -37,12 +38,14 @@ import okio.source /** * Utilities for constructing Artist and Genre images. + * * @author Alexander Capehart (OxygenCobalt), Karim Abou Zeid */ object Images { /** * Create a mosaic image from the given image [InputStream]s. Derived from phonograph: * https://github.com/kabouzeid/Phonograph + * * @param context [Context] required to generate the mosaic. * @param streams [InputStream]s of image data to create the mosaic out of. * @param size [Size] of the Mosaic to generate. @@ -104,8 +107,9 @@ object Images { /** * Get an image dimension suitable to create a mosaic with. + * * @return A pixel dimension derived from the given [Dimension] that will always be even, - * allowing it to be sub-divided. + * allowing it to be sub-divided. */ private fun Dimension.mosaicSize(): Int { val size = pxOrElse { 512 } diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareFrameTransform.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareFrameTransform.kt index 1e31a237e..bdc48b49a 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareFrameTransform.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/SquareFrameTransform.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * SquareFrameTransform.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 @@ -26,6 +27,7 @@ import kotlin.math.min /** * A transformation that performs a center crop-style transformation on an image. Allowing this * behavior to be intrinsic without any view configuration. + * * @author Alexander Capehart (OxygenCobalt) */ class SquareFrameTransform : Transformation { diff --git a/app/src/main/java/org/oxycblt/auxio/list/Data.kt b/app/src/main/java/org/oxycblt/auxio/list/Data.kt index e77d0afb4..5fed1627d 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Data.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Data.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * Data.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 @@ -24,6 +25,7 @@ interface Item /** * A "header" used for delimiting groups of data. + * * @author Alexander Capehart (OxygenCobalt) */ interface Header : Item { @@ -33,6 +35,7 @@ interface Header : Item { /** * A basic header with no additional actions. + * * @param titleRes The string resource used for the header's title. * @author Alexander Capehart (OxygenCobalt) */ 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 2363fc059..dc2393772 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * ListFragment.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 @@ -36,6 +37,7 @@ import org.oxycblt.auxio.util.showToast /** * A Fragment containing a selectable list. + * * @author Alexander Capehart (OxygenCobalt) */ abstract class ListFragment : @@ -52,6 +54,7 @@ abstract class ListFragment : /** * 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]. + * * @param item The [T] data of the item that was clicked. */ abstract fun onRealClick(item: T) @@ -73,6 +76,7 @@ abstract class ListFragment : /** * Opens a menu in the context of a [Song]. 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 song The [Song] to create the menu for. @@ -111,6 +115,7 @@ abstract class ListFragment : /** * Opens a menu in the context of a [Album]. 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 album The [Album] to create the menu for. @@ -147,6 +152,7 @@ abstract class ListFragment : /** * Opens a menu in the context of a [Artist]. 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 artist The [Artist] to create the menu for. @@ -180,6 +186,7 @@ abstract class ListFragment : /** * Opens a menu in the context of a [Genre]. 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 genre The [Genre] to create the menu for. @@ -226,6 +233,7 @@ abstract class ListFragment : /** * 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. 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 1afa340c3..c102fcfef 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * Listeners.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 @@ -23,11 +24,13 @@ import androidx.recyclerview.widget.RecyclerView /** * A basic listener for list interactions. + * * @author Alexander Capehart (OxygenCobalt) */ interface ClickableListListener { /** * Called when an item in the list is clicked. + * * @param item The [T] item that was clicked. * @param viewHolder The [RecyclerView.ViewHolder] of the item that was clicked. */ @@ -35,10 +38,11 @@ interface ClickableListListener { /** * Binds this instance to a list item. + * * @param item The [T] to bind this item to. * @param viewHolder The [RecyclerView.ViewHolder] of the item that was clicked. * @param bodyView The [View] containing the main body of the list item. Any click events on - * this [View] are routed to the listener. Defaults to the root view. + * this [View] are routed to the listener. Defaults to the root view. */ fun bind(item: T, viewHolder: RecyclerView.ViewHolder, bodyView: View = viewHolder.itemView) { bodyView.setOnClickListener { onClick(item, viewHolder) } @@ -47,21 +51,24 @@ interface ClickableListListener { /** * An extension of [ClickableListListener] that enables list editing functionality. + * * @author Alexander Capehart (OxygenCobalt) */ interface EditableListListener : ClickableListListener { /** * Called when a [RecyclerView.ViewHolder] requests that it should be dragged. + * * @param viewHolder The [RecyclerView.ViewHolder] that should start being dragged. */ fun onPickUp(viewHolder: RecyclerView.ViewHolder) /** * Binds this instance to a list item. + * * @param item The [T] to bind this item to. * @param viewHolder The [RecyclerView.ViewHolder] to bind. * @param bodyView The [View] containing the main body of the list item. Any click events on - * this [View] are routed to the listener. Defaults to the root view. + * this [View] are routed to the listener. Defaults to the root view. * @param dragHandle A touchable [View]. Any drag on this view will start a drag event. */ fun bind( @@ -83,11 +90,13 @@ interface EditableListListener : ClickableListListener { /** * An extension of [ClickableListListener] that enables menu and selection functionality. + * * @author Alexander Capehart (OxygenCobalt) */ 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. */ @@ -95,16 +104,18 @@ interface SelectableListListener : ClickableListListener { /** * Called when an item in the list requests that it be selected. + * * @param item The [T] item to select. */ fun onSelect(item: T) /** * Binds this instance to a list item. + * * @param item The [T] to bind this item to. * @param viewHolder The [RecyclerView.ViewHolder] to bind. * @param bodyView The [View] containing the main body of the list item. Any click events on - * this [View] are routed to the listener. Defaults to the root view. + * this [View] are routed to the listener. Defaults to the root view. * @param menuButton A clickable [View]. Any click events on this [View] will open a menu. */ fun bind( diff --git a/app/src/main/java/org/oxycblt/auxio/list/Sort.kt b/app/src/main/java/org/oxycblt/auxio/list/Sort.kt index 941d7ffc5..ec64cdb3c 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Sort.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * Sort.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 @@ -38,6 +39,7 @@ import org.oxycblt.auxio.music.metadata.Disc 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. */ @@ -45,6 +47,7 @@ data class Sort(val mode: Mode, val direction: 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. */ @@ -52,6 +55,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Sort a list of [Song]s. + * * @param songs The list of [Song]s. * @return A new list of [Song]s sorted by this [Sort]'s configuration. */ @@ -63,6 +67,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Sort a list of [Album]s. + * * @param albums The list of [Album]s. * @return A new list of [Album]s sorted by this [Sort]'s configuration. */ @@ -74,6 +79,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Sort a list of [Artist]s. + * * @param artists The list of [Artist]s. * @return A new list of [Artist]s sorted by this [Sort]'s configuration. */ @@ -85,6 +91,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Sort a list of [Genre]s. + * * @param genres The list of [Genre]s. * @return A new list of [Genre]s sorted by this [Sort]'s configuration. */ @@ -96,6 +103,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Sort a *mutable* list of [Song]s in-place using this [Sort]'s configuration. + * * @param songs The [Song]s to sort. */ private fun songsInPlace(songs: MutableList) { @@ -104,6 +112,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Sort a *mutable* list of [Album]s in-place using this [Sort]'s configuration. + * * @param albums The [Album]s to sort. */ private fun albumsInPlace(albums: MutableList) { @@ -112,6 +121,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Sort a *mutable* list of [Artist]s in-place using this [Sort]'s configuration. + * * @param artists The [Album]s to sort. */ private fun artistsInPlace(artists: MutableList) { @@ -120,6 +130,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Sort a *mutable* list of [Genre]s in-place using this [Sort]'s configuration. + * * @param genres The [Genre]s to sort. */ private fun genresInPlace(genres: MutableList) { @@ -128,6 +139,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * The integer representation of this instance. + * * @see fromIntCode */ val intCode: Int @@ -150,6 +162,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Get a [Comparator] that sorts [Song]s according to this [Mode]. + * * @param direction The direction to sort in. * @return A [Comparator] that can be used to sort a [Song] list according to this [Mode]. */ @@ -159,6 +172,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Get a [Comparator] that sorts [Album]s according to this [Mode]. + * * @param direction The direction to sort in. * @return A [Comparator] that can be used to sort a [Album] list according to this [Mode]. */ @@ -168,6 +182,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Return a [Comparator] that sorts [Artist]s according to this [Mode]. + * * @param direction The direction to sort in. * @return A [Comparator] that can be used to sort a [Artist] list according to this [Mode]. */ @@ -177,6 +192,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Return a [Comparator] that sorts [Genre]s according to this [Mode]. + * * @param direction The direction to sort in. * @return A [Comparator] that can be used to sort a [Genre] list according to this [Mode]. */ @@ -186,7 +202,8 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Sort by the item's name. - * @see Music.collationKey + * + * @see Music.sortName */ object ByName : Mode() { override val intCode: Int @@ -210,6 +227,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Sort by the [Album] of an item. Only available for [Song]s. + * * @see Album.collationKey */ object ByAlbum : Mode() { @@ -229,7 +247,8 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Sort by the [Artist] name of an item. Only available for [Song] and [Album]. - * @see Artist.collationKey + * + * @see Artist.sortName */ object ByArtist : Mode() { override val intCode: Int @@ -256,6 +275,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Sort by the [Date] of an item. Only available for [Song] and [Album]. + * * @see Song.date * @see Album.dates */ @@ -308,6 +328,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Sort by the amount of songs an item contains. Only available for [MusicParent]s. + * * @see MusicParent.songs */ object ByCount : Mode() { @@ -333,6 +354,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Sort by the disc number of an item. Only available for [Song]s. + * * @see Song.disc */ object ByDisc : Mode() { @@ -351,6 +373,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Sort by the track number of an item. Only available for [Song]s. + * * @see Song.track */ object ByTrack : Mode() { @@ -369,6 +392,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Sort by the date an item was added. Only supported by [Song]s and [Album]s. + * * @see Song.dateAdded * @see Album.dates */ @@ -391,6 +415,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Utility function to create a [Comparator] in a dynamic way determined by [direction]. + * * @param direction The [Direction] to sort in. * @see compareBy * @see compareByDescending @@ -406,6 +431,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Utility function to create a [Comparator] in a dynamic way determined by [direction] + * * @param direction The [Direction] to sort in. * @param comparator A [Comparator] to wrap. * @return A new [Comparator] with the specified configuration. @@ -419,6 +445,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Utility function to create a [Comparator] a dynamic way determined by [direction] + * * @param direction The [Direction] to sort in. * @param comparator A [Comparator] to wrap. * @param selector Called to obtain a specific attribute to sort by. @@ -439,6 +466,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Utility function to create a [Comparator] that sorts in ascending order based on the * given [Comparator], with a selector based on the item itself. + * * @param comparator The [Comparator] to wrap. * @return A new [Comparator] with the specified configuration. * @see compareBy @@ -448,8 +476,9 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * A [Comparator] that chains several other [Comparator]s together to form one comparison. + * * @param comparators The [Comparator]s to chain. These will be iterated through in order - * during a comparison, with the first non-equal result becoming the result. + * during a comparison, with the first non-equal result becoming the result. */ private class MultiComparator(vararg comparators: Comparator) : Comparator { private val _comparators = comparators @@ -468,6 +497,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * Wraps a [Comparator], extending it to compare two lists. + * * @param inner The [Comparator] to use. */ private class ListComparator(private val inner: Comparator) : Comparator> { @@ -500,13 +530,14 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * A [Comparator] that compares abstract [Music] values. Internally, this is similar to * [NullableComparator], however comparing [Music.collationKey] instead of [Comparable]. + * * @see NullableComparator * @see Music.collationKey */ private class BasicComparator private constructor() : Comparator { override fun compare(a: T, b: T): Int { - val aKey = a.collationKey - val bKey = b.collationKey + val aKey = a.sortName + val bKey = b.sortName return when { aKey != null && bKey != null -> aKey.compareTo(bKey) aKey == null && bKey != null -> -1 // a < b @@ -555,6 +586,7 @@ data class Sort(val mode: Mode, val direction: Direction) { companion object { /** * Convert a [Mode] integer representation into an instance. + * * @param intCode An integer representation of a [Mode] * @return The corresponding [Mode], or null if the [Mode] is invalid. * @see intCode @@ -575,6 +607,7 @@ data class Sort(val mode: Mode, val direction: Direction) { /** * 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 @@ -604,6 +637,7 @@ data class Sort(val mode: Mode, val direction: Direction) { companion object { /** * Convert a [Sort] integer representation into an instance. + * * @param intCode An integer representation of a [Sort] * @return The corresponding [Sort], or null if the [Sort] is invalid. * @see intCode diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/DiffAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/DiffAdapter.kt deleted file mode 100644 index 23e49344d..000000000 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/DiffAdapter.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.list.adapter - -import androidx.recyclerview.widget.RecyclerView - -/** - * A [RecyclerView.Adapter] with [ListDiffer] integration. - * @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use. - */ -abstract class DiffAdapter( - differFactory: ListDiffer.Factory -) : RecyclerView.Adapter() { - private val differ = differFactory.new(@Suppress("LeakingThis") this) - - final override fun getItemCount() = differ.currentList.size - - /** The current list of [T] items. */ - val currentList: List - get() = differ.currentList - - /** - * Get a [T] item at the given position. - * @param at The position to get the item at. - * @throws IndexOutOfBoundsException If the index is not in the list bounds/ - */ - fun getItem(at: Int) = differ.currentList[at] - - /** - * Dynamically determine how to update the list based on the given instructions. - * @param newList The new list of [T] items to show. - * @param instructions The instructions specifying how to update the list. - * @param onDone Called when the update process is completed. Defaults to a no-op. - */ - fun submitList(newList: List, instructions: I, onDone: () -> Unit = {}) { - differ.submitList(newList, instructions, onDone) - } -} 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 new file mode 100644 index 000000000..63096dbf5 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/FlexibleListAdapter.kt @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2023 Auxio Project + * FlexibleListAdapter.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.adapter + +import android.os.Handler +import android.os.Looper +import androidx.recyclerview.widget.* +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import java.util.concurrent.Executor + +/** + * A variant of ListDiffer with more flexible updates. + * + * @param diffCallback A [DiffUtil.ItemCallback] to compare list updates with. + * @author Alexander Capehart (OxygenCobalt) + */ +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 + get() = differ.currentList + /** @see currentList */ + fun getItem(at: Int) = differ.currentList[at] + + /** + * Update the adapter with new data. + * + * @param newData The new list of data to update with. + * @param instructions The [UpdateInstructions] to visually update the list with. + * @param callback Called when the update is completed. May be done asynchronously. + */ + fun update( + newData: List, + instructions: UpdateInstructions?, + callback: (() -> Unit)? = null + ) = differ.update(newData, instructions, callback) +} + +/** + * Arbitrary instructions that can be given to a [FlexibleListAdapter] to direct how it updates + * data. + * + * @author Alexander Capehart (OxygenCobalt) + */ +sealed class UpdateInstructions { + /** Use an asynchronous diff. Useful for unpredictable updates, but looks chaotic and janky. */ + object Diff : UpdateInstructions() + + /** + * Visually replace all items from a given point. More visually coherent than [Diff]. + * + * @param from The index at which to start replacing items (inclusive) + */ + data class Replace(val from: Int) : UpdateInstructions() + + /** + * Add a new set of items. + * + * @param at The position at which to add. + * @param size The amount of items to add. + */ + data class Add(val at: Int, val size: Int) : UpdateInstructions() + + /** + * Move one item to another location. + * + * @param from The index of the item to move. + * @param to The index to move the item to. + */ + data class Move(val from: Int, val to: Int) : UpdateInstructions() + + /** + * Remove an item. + * + * @param at The location that the item should be removed from. + */ + data class Remove(val at: Int) : UpdateInstructions() +} + +/** + * Vendor of AsyncListDiffer with more flexible update functionality. + * + * @author Alexander Capehart (OxygenCobalt) + */ +private class FlexibleListDiffer( + adapter: RecyclerView.Adapter<*>, + diffCallback: DiffUtil.ItemCallback +) { + private val updateCallback = AdapterListUpdateCallback(adapter) + private val config = AsyncDifferConfig.Builder(diffCallback).build() + private val mainThreadExecutor = sMainThreadExecutor + + private class MainThreadExecutor : Executor { + val mHandler = Handler(Looper.getMainLooper()) + override fun execute(command: Runnable) { + mHandler.post(command) + } + } + + var currentList = emptyList() + private set + + private var maxScheduledGeneration = 0 + + fun update(newList: List, instructions: UpdateInstructions?, callback: (() -> Unit)?) { + // incrementing generation means any currently-running diffs are discarded when they finish + val runGeneration = ++maxScheduledGeneration + when (instructions) { + is UpdateInstructions.Replace -> { + updateCallback.onRemoved(instructions.from, currentList.size - instructions.from) + currentList = newList + if (newList.lastIndex >= instructions.from) { + // Need to re-insert the new data. + updateCallback.onInserted(instructions.from, newList.size - instructions.from) + } + callback?.invoke() + } + is UpdateInstructions.Add -> { + currentList = newList + updateCallback.onInserted(instructions.at, instructions.size) + callback?.invoke() + } + is UpdateInstructions.Move -> { + currentList = newList + updateCallback.onMoved(instructions.from, instructions.to) + callback?.invoke() + } + is UpdateInstructions.Remove -> { + currentList = newList + updateCallback.onRemoved(instructions.at, 1) + callback?.invoke() + } + is UpdateInstructions.Diff, + null -> diffList(currentList, newList, runGeneration, callback) + } + } + + private fun diffList( + oldList: List, + newList: List, + runGeneration: Int, + callback: (() -> Unit)? + ) { + // fast simple remove all + if (newList.isEmpty()) { + val countRemoved = oldList.size + currentList = emptyList() + // notify last, after list is updated + updateCallback.onRemoved(0, countRemoved) + callback?.invoke() + return + } + + // fast simple first insert + if (oldList.isEmpty()) { + currentList = newList + // notify last, after list is updated + updateCallback.onInserted(0, newList.size) + callback?.invoke() + return + } + + config.backgroundThreadExecutor.execute { + val result = + DiffUtil.calculateDiff( + object : DiffUtil.Callback() { + override fun getOldListSize(): Int { + return oldList.size + } + + override fun getNewListSize(): Int { + return newList.size + } + + override fun areItemsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ): Boolean { + val oldItem: T? = oldList[oldItemPosition] + val newItem: T? = newList[newItemPosition] + return if (oldItem != null && newItem != null) { + config.diffCallback.areItemsTheSame(oldItem, newItem) + } else oldItem == null && newItem == null + // If both items are null we consider them the same. + } + + override fun areContentsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ): Boolean { + val oldItem: T? = oldList[oldItemPosition] + val newItem: T? = newList[newItemPosition] + if (oldItem != null && newItem != null) { + return config.diffCallback.areContentsTheSame(oldItem, newItem) + } + if (oldItem == null && newItem == null) { + return true + } + throw AssertionError() + } + + override fun getChangePayload( + oldItemPosition: Int, + newItemPosition: Int + ): Any? { + val oldItem: T? = oldList[oldItemPosition] + val newItem: T? = newList[newItemPosition] + if (oldItem != null && newItem != null) { + return config.diffCallback.getChangePayload(oldItem, newItem) + } + throw AssertionError() + } + }) + mainThreadExecutor.execute { + if (maxScheduledGeneration == runGeneration) { + currentList = newList + result.dispatchUpdatesTo(updateCallback) + callback?.invoke() + } + } + } + } + + companion object { + private val sMainThreadExecutor: Executor = MainThreadExecutor() + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/ListDiffer.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/ListDiffer.kt deleted file mode 100644 index 7c5207e6c..000000000 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/ListDiffer.kt +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.list.adapter - -import androidx.recyclerview.widget.AdapterListUpdateCallback -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.recyclerview.widget.AsyncListDiffer -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListUpdateCallback -import androidx.recyclerview.widget.RecyclerView - -// TODO: Re-add list instructions with a less dangerous framework. - -/** - * List differ wrapper that provides more flexibility regarding the way lists are updated. - * @author Alexander Capehart (OxygenCobalt) - */ -interface ListDiffer { - /** The current list of [T] items. */ - val currentList: List - - /** - * Dynamically determine how to update the list based on the given instructions. - * @param newList The new list of [T] items to show. - * @param instructions The [BasicListInstructions] specifying how to update the list. - * @param onDone Called when the update process is completed. - */ - fun submitList(newList: List, instructions: I, onDone: () -> Unit) - - /** - * Defines the creation of new [ListDiffer] instances. Allows such [ListDiffer]s to be passed as - * arguments without reliance on a `this` [RecyclerView.Adapter]. - */ - abstract class Factory { - /** - * Create a new [ListDiffer] bound to the given [RecyclerView.Adapter]. - * @param adapter The [RecyclerView.Adapter] to bind to. - */ - abstract fun new(adapter: RecyclerView.Adapter<*>): ListDiffer - } - - /** - * Update lists on another thread. This is useful when large diffs are likely to occur in this - * list that would be exceedingly slow with [Blocking]. - * @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the - * internal list. - */ - class Async(private val diffCallback: DiffUtil.ItemCallback) : - Factory() { - override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer = - AsyncListDifferImpl(AdapterListUpdateCallback(adapter), diffCallback) - } - - /** - * Update lists on the main thread. This is useful when many small, discrete list diffs are - * likely to occur that would cause [Async] to suffer from race conditions. - * @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the - * internal list. - */ - class Blocking(private val diffCallback: DiffUtil.ItemCallback) : - Factory() { - override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer = - BlockingListDifferImpl(AdapterListUpdateCallback(adapter), diffCallback) - } -} - -/** - * Represents the specific way to update a list of items. - * @author Alexander Capehart (OxygenCobalt) - */ -enum class BasicListInstructions { - /** - * (A)synchronously diff the list. This should be used for small diffs with little item - * movement. - */ - DIFF, - - /** - * Synchronously remove the current list and replace it with a new one. This should be used for - * large diffs with that would cause erratic scroll behavior or in-efficiency. - */ - REPLACE -} - -private abstract class BasicListDiffer : ListDiffer { - override fun submitList( - newList: List, - instructions: BasicListInstructions, - onDone: () -> Unit - ) { - when (instructions) { - BasicListInstructions.DIFF -> diffList(newList, onDone) - BasicListInstructions.REPLACE -> replaceList(newList, onDone) - } - } - - protected abstract fun diffList(newList: List, onDone: () -> Unit) - protected abstract fun replaceList(newList: List, onDone: () -> Unit) -} - -private class AsyncListDifferImpl( - updateCallback: ListUpdateCallback, - diffCallback: DiffUtil.ItemCallback -) : BasicListDiffer() { - private val inner = - AsyncListDiffer(updateCallback, AsyncDifferConfig.Builder(diffCallback).build()) - - override val currentList: List - get() = inner.currentList - - override fun diffList(newList: List, onDone: () -> Unit) { - inner.submitList(newList, onDone) - } - - override fun replaceList(newList: List, onDone: () -> Unit) { - inner.submitList(null) { inner.submitList(newList, onDone) } - } -} - -private class BlockingListDifferImpl( - private val updateCallback: ListUpdateCallback, - private val diffCallback: DiffUtil.ItemCallback -) : BasicListDiffer() { - override var currentList = listOf() - - override fun diffList(newList: List, onDone: () -> Unit) { - if (newList === currentList || newList.isEmpty() && currentList.isEmpty()) { - onDone() - return - } - - if (newList.isEmpty()) { - val oldListSize = currentList.size - currentList = listOf() - updateCallback.onRemoved(0, oldListSize) - onDone() - return - } - - if (currentList.isEmpty()) { - currentList = newList - updateCallback.onInserted(0, newList.size) - onDone() - return - } - - val oldList = currentList - val result = - DiffUtil.calculateDiff( - object : DiffUtil.Callback() { - override fun getOldListSize(): Int { - return oldList.size - } - - override fun getNewListSize(): Int { - return newList.size - } - - override fun areItemsTheSame( - oldItemPosition: Int, - newItemPosition: Int - ): Boolean { - val oldItem: T? = oldList[oldItemPosition] - val newItem: T? = newList[newItemPosition] - return if (oldItem != null && newItem != null) { - diffCallback.areItemsTheSame(oldItem, newItem) - } else { - oldItem == null && newItem == null - } - } - - override fun areContentsTheSame( - oldItemPosition: Int, - newItemPosition: Int - ): Boolean { - val oldItem: T? = oldList[oldItemPosition] - val newItem: T? = newList[newItemPosition] - return if (oldItem != null && newItem != null) { - diffCallback.areContentsTheSame(oldItem, newItem) - } else if (oldItem == null && newItem == null) { - true - } else { - throw AssertionError() - } - } - - override fun getChangePayload( - oldItemPosition: Int, - newItemPosition: Int - ): Any? { - val oldItem: T? = oldList[oldItemPosition] - val newItem: T? = newList[newItemPosition] - return if (oldItem != null && newItem != null) { - diffCallback.getChangePayload(oldItem, newItem) - } else { - throw AssertionError() - } - } - }) - - currentList = newList - result.dispatchUpdatesTo(updateCallback) - onDone() - } - - override fun replaceList(newList: List, onDone: () -> Unit) { - if (currentList != newList) { - diffList(listOf()) { diffList(newList, onDone) } - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt index 588c5c9b6..67fccceac 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * PlayingIndicatorAdapter.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 @@ -18,17 +19,19 @@ package org.oxycblt.auxio.list.adapter import android.view.View +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.util.logD /** * A [RecyclerView.Adapter] that supports indicating the playback status of a particular item. - * @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use. + * + * @param diffCallback A [DiffUtil.ItemCallback] to compare list updates with. * @author Alexander Capehart (OxygenCobalt) */ -abstract class PlayingIndicatorAdapter( - differFactory: ListDiffer.Factory -) : DiffAdapter(differFactory) { +abstract class PlayingIndicatorAdapter( + diffCallback: DiffUtil.ItemCallback +) : FlexibleListAdapter(diffCallback) { // There are actually two states for this adapter: // - The currently playing item, which is usually marked as "selected" and becomes accented. // - Whether playback is ongoing, which corresponds to whether the item's ImageGroup is @@ -39,7 +42,7 @@ abstract class PlayingIndicatorAdapter( override fun onBindViewHolder(holder: VH, position: Int, payloads: List) { // Only try to update the playing indicator if the ViewHolder supports it if (holder is ViewHolder) { - holder.updatePlayingIndicator(currentList[position] == currentItem, isPlaying) + holder.updatePlayingIndicator(getItem(position) == currentItem, isPlaying) } if (payloads.isEmpty()) { @@ -50,6 +53,7 @@ abstract class PlayingIndicatorAdapter( } /** * Update the currently playing item in the list. + * * @param item The [T] currently being played, or null if it is not being played. * @param isPlaying Whether playback is ongoing or paused. */ @@ -103,9 +107,10 @@ abstract class PlayingIndicatorAdapter( abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) { /** * Update the playing indicator within this [RecyclerView.ViewHolder]. + * * @param isActive True if this item is playing, false otherwise. * @param isPlaying True if playback is ongoing, false if paused. If this is true, - * [isActive] will also be true. + * [isActive] will also be true. */ abstract fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) } diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt index 20239546c..641e8b2b3 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * SelectionIndicatorAdapter.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 @@ -18,18 +19,20 @@ package org.oxycblt.auxio.list.adapter import android.view.View +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.music.Music /** * A [PlayingIndicatorAdapter] that also supports indicating the selection status of a group of * items. - * @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use. + * + * @param diffCallback A [DiffUtil.ItemCallback] to compare list updates with. * @author Alexander Capehart (OxygenCobalt) */ -abstract class SelectionIndicatorAdapter( - differFactory: ListDiffer.Factory -) : PlayingIndicatorAdapter(differFactory) { +abstract class SelectionIndicatorAdapter( + diffCallback: DiffUtil.ItemCallback +) : PlayingIndicatorAdapter(diffCallback) { private var selectedItems = setOf() override fun onBindViewHolder(holder: VH, position: Int, payloads: List) { @@ -41,6 +44,7 @@ abstract class SelectionIndicatorAdapter( /** * Update the list of selected items. + * * @param items A set of selected [T] items. */ fun setSelected(items: Set) { @@ -62,9 +66,7 @@ abstract class SelectionIndicatorAdapter( } // Only update items that were added or removed from the list. - val added = !oldSelectedItems.contains(item) && newSelectedItems.contains(item) - val removed = oldSelectedItems.contains(item) && !newSelectedItems.contains(item) - if (added || removed) { + if (oldSelectedItems.contains(item) xor newSelectedItems.contains(item)) { notifyItemChanged(i, PAYLOAD_SELECTION_INDICATOR_CHANGED) } } @@ -74,6 +76,7 @@ abstract class SelectionIndicatorAdapter( abstract class ViewHolder(root: View) : PlayingIndicatorAdapter.ViewHolder(root) { /** * Update the selection indicator within this [PlayingIndicatorAdapter.ViewHolder]. + * * @param isSelected Whether this [PlayingIndicatorAdapter.ViewHolder] is selected. */ abstract fun updateSelectionIndicator(isSelected: Boolean) diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/SimpleDiffCallback.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/SimpleDiffCallback.kt index 358c3b3f1..d1fdc8a7a 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/adapter/SimpleDiffCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/SimpleDiffCallback.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * SimpleDiffCallback.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 @@ -23,6 +24,7 @@ import org.oxycblt.auxio.list.Item /** * A [DiffUtil.ItemCallback] that automatically implements the [areItemsTheSame] method. Use this * whenever creating [DiffUtil.ItemCallback] implementations with an [Item] subclass. + * * @author Alexander Capehart (OxygenCobalt) */ abstract class SimpleDiffCallback : DiffUtil.ItemCallback() { diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/AuxioRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/AuxioRecyclerView.kt index 06eea4411..c84f3176e 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/AuxioRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/AuxioRecyclerView.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * AuxioRecyclerView.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 @@ -31,6 +32,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat * - Automatic edge-to-edge support * - Adapter-based [SpanSizeLookup] implementation * - Automatic [setHasFixedSize] setup + * * @author Alexander Capehart (OxygenCobalt) */ open class AuxioRecyclerView @@ -89,6 +91,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr interface SpanSizeLookup { /** * Get if the item at a position takes up the whole width of the [RecyclerView] or not. + * * @param position The position of the item. * @return true if the item is full-width, false otherwise. */ 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 6984f1c93..96ef1ffcd 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * DialogRecyclerView.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 @@ -34,6 +35,7 @@ import org.oxycblt.auxio.util.getDimenPixels * A [RecyclerView] intended for use in Dialogs, adding features such as: * - NestedScrollView scrollIndicators behavior emulation * - Dialog-specific [ViewHolder] that automatically resolves certain issues. + * * @author Alexander Capehart (OxygenCobalt) */ class DialogRecyclerView diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/HeaderItemDecoration.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/HeaderItemDecoration.kt index b715b8b70..1ffc6c1fc 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/HeaderItemDecoration.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/HeaderItemDecoration.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * HeaderItemDecoration.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 @@ -19,16 +20,18 @@ package org.oxycblt.auxio.list.recycler import android.content.Context import android.util.AttributeSet +import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.divider.BackportMaterialDividerItemDecoration import org.oxycblt.auxio.R import org.oxycblt.auxio.list.Header -import org.oxycblt.auxio.list.adapter.DiffAdapter +import org.oxycblt.auxio.list.adapter.FlexibleListAdapter /** * A [BackportMaterialDividerItemDecoration] that sets up the divider configuration to correctly * separate content with headers. + * * @author Alexander Capehart (OxygenCobalt) */ class HeaderItemDecoration @@ -39,12 +42,26 @@ constructor( defStyleAttr: Int = R.attr.materialDividerStyle, orientation: Int = LinearLayoutManager.VERTICAL ) : BackportMaterialDividerItemDecoration(context, attributeSet, defStyleAttr, orientation) { - override fun shouldDrawDivider(position: Int, adapter: RecyclerView.Adapter<*>?) = + override fun shouldDrawDivider(position: Int, adapter: RecyclerView.Adapter<*>?): Boolean { + if (adapter is ConcatAdapter) { + val adapterAndPosition = + try { + adapter.getWrappedAdapterAndPosition(position + 1) + } catch (e: IllegalArgumentException) { + return false + } + return hasHeaderAtPosition(adapterAndPosition.second, adapterAndPosition.first) + } else { + return hasHeaderAtPosition(position + 1, adapter) + } + } + + private fun hasHeaderAtPosition(position: Int, adapter: RecyclerView.Adapter<*>?) = try { // Add a divider if the next item is a header. This organizes the divider to separate // the ends of content rather than the beginning of content, alongside an added benefit // of preventing top headers from having a divider applied. - (adapter as DiffAdapter<*, *, *>).getItem(position + 1) is Header + (adapter as FlexibleListAdapter<*, *>).getItem(position) is Header } catch (e: ClassCastException) { false } catch (e: IndexOutOfBoundsException) { diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt index ff09af8b1..1f5188c4f 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * ViewHolders.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 @@ -36,12 +37,14 @@ import org.oxycblt.auxio.util.logD /** * A [RecyclerView.ViewHolder] that displays a [Song]. Use [from] to create an instance. + * * @author Alexander Capehart (OxygenCobalt) */ class SongViewHolder private constructor(private val binding: ItemSongBinding) : SelectionIndicatorAdapter.ViewHolder(binding.root) { /** * Bind new data to this instance. + * * @param song The new [Song] to bind. * @param listener An [SelectableListListener] to bind interactions to. */ @@ -67,6 +70,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) : /** * Create a new instance. + * * @param parent The parent to inflate this instance from. * @return A new instance. */ @@ -84,12 +88,14 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) : /** * A [RecyclerView.ViewHolder] that displays a [Album]. Use [from] to create an instance. + * * @author Alexander Capehart (OxygenCobalt) */ class AlbumViewHolder private constructor(private val binding: ItemParentBinding) : SelectionIndicatorAdapter.ViewHolder(binding.root) { /** * Bind new data to this instance. + * * @param album The new [Album] to bind. * @param listener An [SelectableListListener] to bind interactions to. */ @@ -115,6 +121,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding /** * Create a new instance. + * * @param parent The parent to inflate this instance from. * @return A new instance. */ @@ -133,12 +140,14 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding /** * A [RecyclerView.ViewHolder] that displays a [Artist]. Use [from] to create an instance. + * * @author Alexander Capehart (OxygenCobalt) */ class ArtistViewHolder private constructor(private val binding: ItemParentBinding) : SelectionIndicatorAdapter.ViewHolder(binding.root) { /** * Bind new data to this instance. + * * @param artist The new [Artist] to bind. * @param listener An [SelectableListListener] to bind interactions to. */ @@ -173,6 +182,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin /** * Create a new instance. + * * @param parent The parent to inflate this instance from. * @return A new instance. */ @@ -192,12 +202,14 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin /** * A [RecyclerView.ViewHolder] that displays a [Genre]. Use [from] to create an instance. + * * @author Alexander Capehart (OxygenCobalt) */ class GenreViewHolder private constructor(private val binding: ItemParentBinding) : SelectionIndicatorAdapter.ViewHolder(binding.root) { /** * Bind new data to this instance. + * * @param genre The new [Genre] to bind. * @param listener An [SelectableListListener] to bind interactions to. */ @@ -227,6 +239,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding /** * Create a new instance. + * * @param parent The parent to inflate this instance from. * @return A new instance. */ @@ -243,12 +256,14 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding /** * A [RecyclerView.ViewHolder] that displays a [BasicHeader]. Use [from] to create an instance. + * * @author Alexander Capehart (OxygenCobalt) */ class BasicHeaderViewHolder private constructor(private val binding: ItemHeaderBinding) : RecyclerView.ViewHolder(binding.root) { /** * Bind new data to this instance. + * * @param basicHeader The new [BasicHeader] to bind. */ fun bind(basicHeader: BasicHeader) { @@ -262,6 +277,7 @@ class BasicHeaderViewHolder private constructor(private val binding: ItemHeaderB /** * Create a new instance. + * * @param parent The parent to inflate this instance from. * @return A new instance. */ diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt index b9912bc09..a3012f56b 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * SelectionFragment.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 @@ -28,6 +29,7 @@ import org.oxycblt.auxio.util.showToast /** * A subset of ListFragment that implements aspects of the selection UI. + * * @author Alexander Capehart (OxygenCobalt) */ abstract class SelectionFragment : @@ -38,8 +40,9 @@ abstract class SelectionFragment : /** * Get the [SelectionToolbarOverlay] of the concrete Fragment to be automatically managed by * [SelectionFragment]. + * * @return The [SelectionToolbarOverlay] of the concrete [SelectionFragment]'s [VB], or null if - * there is not one. + * there is not one. */ open fun getSelectionToolbar(binding: VB): SelectionToolbarOverlay? = null diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt index 106244edd..05b203771 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * SelectionToolbarOverlay.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 @@ -32,6 +33,7 @@ import org.oxycblt.auxio.util.logD /** * A wrapper around a [MaterialToolbar] that adds an additional [MaterialToolbar] showing the * current selection state. + * * @author Alexander Capehart (OxygenCobalt) */ class SelectionToolbarOverlay @@ -65,6 +67,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr /** * Set an OnClickListener for when the "cancel" button in the selection [MaterialToolbar] is * pressed. + * * @param listener The OnClickListener to respond to this interaction. * @see MaterialToolbar.setNavigationOnClickListener */ @@ -75,6 +78,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr /** * Set an [OnMenuItemClickListener] for when a MenuItem is selected from the selection * [MaterialToolbar]. + * * @param listener The [OnMenuItemClickListener] to respond to this interaction. * @see MaterialToolbar.setOnMenuItemClickListener */ @@ -84,6 +88,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr /** * Update the selection [MaterialToolbar] to reflect the current selection amount. + * * @param amount The amount of items that are currently selected. * @return true if the selection [MaterialToolbar] changes, false otherwise. */ @@ -101,6 +106,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr /** * Animate the visibility of the inner and selection [MaterialToolbar]s to the given state. + * * @param selectionVisible Whether the selection [MaterialToolbar] should be visible or not. * @return true if the toolbars have changed, false otherwise. */ @@ -152,8 +158,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr /** * Update the alpha of the inner and selection [MaterialToolbar]s. + * * @param innerAlpha The opacity of the inner [MaterialToolbar]. This will map to the inverse - * opacity of the selection [MaterialToolbar]. + * opacity of the selection [MaterialToolbar]. */ private fun setToolbarsAlpha(innerAlpha: Float) { innerToolbar.apply { diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt index 66424b1d1..150e8552f 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * SelectionViewModel.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 @@ -27,6 +28,7 @@ import org.oxycblt.auxio.music.model.Library /** * A [ViewModel] that manages the current selection. + * * @author Alexander Capehart (OxygenCobalt) */ @HiltViewModel @@ -67,6 +69,7 @@ class SelectionViewModel @Inject constructor(private val musicRepository: MusicR /** * Select a new [Music] item. If this item is already within the selected items, the item will * be removed. Otherwise, it will be added. + * * @param music The [Music] item to select. */ fun select(music: Music) { @@ -79,6 +82,7 @@ class SelectionViewModel @Inject constructor(private val musicRepository: MusicR /** * Consume the current selection. This will clear any items that were selected prior. + * * @return The list of selected items before it was cleared. */ fun consume() = _selected.value.also { _selected.value = listOf() } diff --git a/app/src/main/java/org/oxycblt/auxio/music/AudioOnlyExtractors.kt b/app/src/main/java/org/oxycblt/auxio/music/AudioOnlyExtractors.kt deleted file mode 100644 index 75eb1bdd1..000000000 --- a/app/src/main/java/org/oxycblt/auxio/music/AudioOnlyExtractors.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.music - -import com.google.android.exoplayer2.extractor.ExtractorsFactory -import com.google.android.exoplayer2.extractor.flac.FlacExtractor -import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor -import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor -import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor -import com.google.android.exoplayer2.extractor.ogg.OggExtractor -import com.google.android.exoplayer2.extractor.ts.AdtsExtractor -import com.google.android.exoplayer2.extractor.wav.WavExtractor - -/** - * A [ExtractorsFactory] that only provides audio containers to save APK space. - * @author Alexander Capehart (OxygenCobalt) - */ -object AudioOnlyExtractors : ExtractorsFactory { - override fun createExtractors() = - arrayOf( - FlacExtractor(), - WavExtractor(), - Mp4Extractor(), - OggExtractor(), - MatroskaExtractor(), - // Enable constant bitrate seeking so that certain MP3s/AACs are seekable - AdtsExtractor(AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING), - Mp3Extractor(Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING)) -} 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 665010655..6df980346 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * Music.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 @@ -22,6 +23,7 @@ import android.net.Uri import android.os.Parcelable import java.security.MessageDigest import java.text.CollationKey +import java.text.Collator import java.util.UUID import kotlin.math.max import kotlinx.parcelize.IgnoredOnParcel @@ -38,11 +40,13 @@ import org.oxycblt.auxio.util.toUuidOrNull /** * Abstract music data. This contains universal information about all concrete music * implementations, such as identification information and names. + * * @author Alexander Capehart (OxygenCobalt) */ sealed interface Music : Item { /** * A unique identifier for this music item. + * * @see UID */ val uid: UID @@ -56,9 +60,10 @@ sealed interface Music : Item { /** * Returns a name suitable for use in the app UI. This should be favored over [rawName] in * nearly all cases. + * * @param context [Context] required to obtain placeholder text or formatting information. * @return A human-readable string representing the name of this music. In the case that the - * item does not have a name, an analogous "Unknown X" name is returned. + * item does not have a name, an analogous "Unknown X" name is returned. */ fun resolveName(context: Context): String @@ -70,31 +75,26 @@ sealed interface Music : Item { val rawSortName: String? /** - * A [CollationKey] derived from [rawName] and [rawSortName] that can be used to sort items in a - * semantically-correct manner. Will be null if the item has no name. - * - * The key will have the following attributes: - * - If [rawSortName] is present, this key will be derived from it. Otherwise [rawName] is used. - * - If the string begins with an article, such as "the", it will be stripped, as is usually - * convention for sorting media. This is not internationalized. + * A black-box value derived from [rawSortName] and [rawName] that can be used for user-friendly + * sorting in the context of music. This should be preferred over [rawSortName] in most cases. + * Null if there are no [rawName] or [rawSortName] values to build on. */ - val collationKey: CollationKey? + val sortName: SortName? /** * A unique identifier for a piece of music. * * [UID] enables a much cheaper and more reliable form of differentiating music, derived from - * either a hash of meaningful metadata or the MusicBrainz ID spec. Using this enables several + * either internal app information or the MusicBrainz ID spec. Using this enables several * improvements to music management in this app, including: - * * - Proper differentiation of identical music. It's common for large, well-tagged libraries to - * have functionally duplicate items that are differentiated with MusicBrainz IDs, and so [UID] - * allows us to properly differentiate between these in the app. + * have functionally duplicate items that are differentiated with MusicBrainz IDs, and so + * [UID] allows us to properly differentiate between these in the app. * - Better music persistence between restarts. Whereas directly storing song names would be - * prone to collisions, and storing MediaStore IDs would drift rapidly as the music library - * changes, [UID] enables a much stronger form of persistence given it's unique link to a - * specific files metadata configuration, which is unlikely to collide with another item or - * drift as the music library changes. + * prone to collisions, and storing MediaStore IDs would drift rapidly as the music library + * changes, [UID] enables a much stronger form of persistence given it's unique link to a + * specific files metadata configuration, which is unlikely to collide with another item or + * drift as the music library changes. * * Note: Generally try to use [UID] as a black box that can only be read, written, and compared. * It will not be fun if you try to manipulate it in any other manner. @@ -125,6 +125,7 @@ sealed interface Music : Item { /** * Internal marker of [Music.UID] format type. + * * @param namespace Namespace to use in the [Music.UID]'s string representation. */ private enum class Format(val namespace: String) { @@ -139,10 +140,11 @@ sealed interface Music : Item { /** * 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 updates Block to update the [MessageDigest] hash with the metadata of the - * item. Make sure the metadata hashed semantically aligns with the format - * specification. + * 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 { @@ -181,19 +183,21 @@ sealed interface Music : Item { /** * 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 mbid The analogous MusicBrainz ID for this item that was extracted from a - * file. + * file. * @return A new MusicBrainz-style [UID]. */ fun musicBrainz(mode: MusicMode, mbid: UUID): UID = UID(Format.MUSICBRAINZ, mode, mbid) /** * Convert a [UID]'s string representation back into a concrete [UID] instance. + * * @param uid The [UID]'s string representation, formatted as - * `format_namespace:music_mode_int-uuid`. + * `format_namespace:music_mode_int-uuid`. * @return A [UID] converted from the string representation, or null if the string - * representation was invalid. + * representation was invalid. */ fun fromString(uid: String): UID? { val split = uid.split(':', limit = 2) @@ -224,6 +228,7 @@ sealed interface Music : Item { /** * An abstract grouping of [Song]s and other [Music] data. + * * @author Alexander Capehart (OxygenCobalt) */ sealed interface MusicParent : Music { @@ -233,6 +238,7 @@ sealed interface MusicParent : Music { /** * A song. + * * @author Alexander Capehart (OxygenCobalt) */ interface Song : Music { @@ -281,6 +287,7 @@ interface Song : Music { /** * An abstract release group. While it may be called an album, it encompasses other types of * releases like singles, EPs, and compilations. + * * @author Alexander Capehart (OxygenCobalt) */ interface Album : MusicParent { @@ -311,6 +318,7 @@ interface Album : MusicParent { /** * An abstract artist. These are actually a combination of the artist and album artist tags from * within the library, derived from [Song]s and [Album]s respectively. + * * @author Alexander Capehart (OxygenCobalt) */ interface Artist : MusicParent { @@ -336,6 +344,7 @@ interface Artist : MusicParent { /** * A genre. + * * @author Alexander Capehart (OxygenCobalt) */ interface Genre : MusicParent { @@ -347,9 +356,84 @@ interface Genre : MusicParent { val durationMs: Long } +/** + * A black-box datatype for a variation of music names that is suitable for music-oriented sorting. + * It will automatically handle articles like "The" and numeric components like "An". + * + * @author Alexander Capehart (OxygenCobalt) + */ +class SortName(name: String, musicSettings: MusicSettings) : Comparable { + private val number: Int? + private val collationKey: CollationKey + val thumbString: String? + + init { + var sortName = name + if (musicSettings.intelligentSorting) { + sortName = + sortName.run { + when { + length > 5 && startsWith("the ", ignoreCase = true) -> substring(4) + length > 4 && startsWith("an ", ignoreCase = true) -> substring(3) + length > 3 && startsWith("a ", ignoreCase = true) -> substring(2) + else -> this + } + } + + // Parse out numeric portions of the title and use those for sorting, if applicable. + when (val numericEnd = sortName.indexOfFirst { !it.isDigit() }) { + // No numeric component. + 0 -> number = null + // Whole title is numeric. + -1 -> { + number = sortName.toIntOrNull() + sortName = "" + } + // Part of the title is numeric. + else -> { + number = sortName.slice(0 until numericEnd).toIntOrNull() + sortName = sortName.slice(numericEnd until sortName.length) + } + } + } else { + number = null + } + + collationKey = COLLATOR.getCollationKey(sortName) + + // Keep track of a string to use in the thumb view. + // TODO: This needs to be moved elsewhere. + thumbString = (number?.toString() ?: collationKey?.run { sourceString.first().uppercase() }) + } + + override fun toString(): String = number?.toString() ?: collationKey.sourceString + + override fun compareTo(other: SortName) = + when { + number != null && other.number != null -> number.compareTo(other.number) + number != null && other.number == null -> -1 // a < b + number == null && other.number != null -> 1 // a > b + else -> collationKey.compareTo(other.collationKey) + } + + override fun equals(other: Any?) = + other is SortName && number == other.number && collationKey == other.collationKey + + override fun hashCode(): Int { + var hashCode = collationKey.hashCode() + if (number != null) hashCode = 31 * hashCode + number + return hashCode + } + + private companion object { + val COLLATOR: Collator = Collator.getInstance().apply { strength = Collator.PRIMARY } + } +} + /** * Run [Music.resolveName] on each instance in the given list and concatenate them into a [String] * in a localized manner. + * * @param context [Context] required * @return A concatenated string. */ @@ -359,6 +443,7 @@ fun List.resolveNames(context: Context) = /** * Returns if [Music.rawName] matches for each item in a list. Useful for scenarios where the * display information of an item must be compared without a context. + * * @param other The list of items to compare to. * @return True if they are the same (by [Music.rawName]), false otherwise. */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt index 42a86f25a..f959e5f7d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicMode.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * MusicMode.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,6 +22,7 @@ import org.oxycblt.auxio.IntegerTable /** * Represents a data configuration corresponding to a specific type of [Music], + * * @author Alexander Capehart (OxygenCobalt) */ enum class MusicMode { @@ -35,6 +37,7 @@ enum class MusicMode { /** * The integer representation of this instance. + * * @see fromIntCode */ val intCode: Int @@ -49,6 +52,7 @@ enum class MusicMode { companion object { /** * Convert a [MusicMode] 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 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 2f91cfdba..58fd9b323 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * MusicModule.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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 9b4f73884..461cb6401 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * MusicRepository.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 @@ -40,6 +41,7 @@ interface MusicRepository { /** * Add a [Listener] to this instance. This can be used to receive changes in the music library. * Will invoke all [Listener] methods to initialize the instance with the current state. + * * @param listener The [Listener] to add. * @see Listener */ @@ -47,8 +49,9 @@ interface MusicRepository { /** * Remove a [Listener] from this instance, preventing it from receiving any further updates. + * * @param listener The [Listener] to remove. Does nothing if the [Listener] was never added in - * the first place. + * the first place. * @see Listener */ fun removeListener(listener: Listener) @@ -57,6 +60,7 @@ interface MusicRepository { interface Listener { /** * Called when the current [Library] has changed. + * * @param library The new [Library], or null if no [Library] has been loaded yet. */ fun onLibraryChanged(library: Library?) 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 252693747..7f986fdbf 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * MusicSettings.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 @@ -31,6 +32,7 @@ import org.oxycblt.auxio.util.getSystemServiceCompat /** * User configuration specific to music system. + * * @author Alexander Capehart (OxygenCobalt) */ interface MusicSettings : Settings { @@ -42,8 +44,9 @@ interface MusicSettings : Settings { val shouldBeObserving: Boolean /** A [String] of characters representing the desired characters to denote multi-value tags. */ var multiValueSeparators: String - /** Whether to trim english articles with song sort names. */ - val automaticSortNames: Boolean + /** 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. */ @@ -108,7 +111,7 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context } } - override val automaticSortNames: Boolean + override val intelligentSorting: Boolean get() = sharedPreferences.getBoolean(getString(R.string.set_key_auto_sort_names), true) override var songSort: Sort 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 a8cae7af8..9b99c7f2e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * MusicViewModel.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 @@ -26,6 +27,7 @@ import org.oxycblt.auxio.music.system.Indexer /** * A [ViewModel] providing data specific to the music loading process. + * * @author Alexander Capehart (OxygenCobalt) */ @HiltViewModel @@ -76,6 +78,7 @@ class MusicViewModel @Inject constructor(private val indexer: Indexer) : /** * Non-manipulated statistics bound the last successful music load. + * * @param songs The amount of [Song]s that were loaded. * @param albums The amount of [Album]s that were created. * @param artists The amount of [Artist]s that were created. 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 a82179730..8e9830aba 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * CacheDatabase.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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt index 4dac9555d..82e70f217 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * CacheModule.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 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 34b19a617..55a58ba74 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * CacheRepository.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 @@ -23,17 +24,20 @@ import org.oxycblt.auxio.util.* /** * A repository allowing access to cached metadata obtained in prior music loading operations. + * * @author Alexander Capehart (OxygenCobalt) */ interface CacheRepository { /** * Read the current [Cache], if it exists. + * * @return The stored [Cache], or null if it could not be obtained. */ suspend fun readCache(): Cache? /** * Write the list of newly-loaded [RawSong]s to the cache, replacing the prior data. + * * @param rawSongs The [rawSongs] to write to the cache. */ suspend fun writeCache(rawSongs: List) @@ -67,6 +71,7 @@ class CacheRepositoryImpl @Inject constructor(private val cachedSongsDao: Cached /** * A cache of music metadata obtained in prior music loading operations. Obtain an instance with * [CacheRepository]. + * * @author Alexander Capehart (OxygenCobalt) */ interface Cache { @@ -75,6 +80,7 @@ interface Cache { /** * Populate a [RawSong] from a cache entry, if it exists. + * * @param rawSong The [RawSong] to populate. * @return true if a cache entry could be applied to [rawSong], false otherwise. */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioInfo.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioInfo.kt index 9411c5cfe..b31b5f63f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioInfo.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/AudioInfo.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * AudioInfo.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 @@ -30,6 +31,7 @@ import org.oxycblt.auxio.util.logW /** * The properties of a [Song]'s file. + * * @param bitrateKbps The bit rate, in kilobytes-per-second. Null if it could not be parsed. * @param sampleRateHz The sample rate, in hertz. * @param resolvedMimeType The known mime type of the [Song] after it's file format was determined. @@ -44,6 +46,7 @@ data class AudioInfo( interface Provider { /** * Extract the [AudioInfo] of a given [Song]. + * * @param song The [Song] to read. * @return The [AudioInfo] of the [Song], if possible to obtain. */ @@ -53,6 +56,7 @@ data class AudioInfo( /** * A framework-backed implementation of [AudioInfo.Provider]. + * * @param context [Context] required to read audio files. */ class AudioInfoProviderImpl @Inject constructor(@ApplicationContext private val context: Context) : diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/Date.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/Date.kt index bd4764390..388a81842 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/Date.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/Date.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * Date.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 @@ -44,10 +45,11 @@ class Date private constructor(private val tokens: List) : Comparable /** * Resolve this instance into a human-readable date. + * * @param context [Context] required to get human-readable names. * @return If the [Date] has a valid month and year value, a more fine-grained date (ex. "Jan - * 2020") will be returned. Otherwise, a plain year value (ex. "2020") is returned. Dates will - * be properly localized. + * 2020") will be returned. Otherwise, a plain year value (ex. "2020") is returned. Dates will + * be properly localized. */ fun resolveDate(context: Context): String { if (month != null) { @@ -115,6 +117,7 @@ class Date private constructor(private val tokens: List) : Comparable * A range of [Date]s. This is used in contexts where the [Date] of an item is derived from * several sub-items and thus can have a "range" of release dates. Use [from] to create an * instance. + * * @author Alexander Capehart */ class Range @@ -127,10 +130,11 @@ class Date private constructor(private val tokens: List) : Comparable /** * Resolve this instance into a human-readable date range. + * * @param context [Context] required to get human-readable names. * @return If the date has a maximum value, then a `min - max` formatted string will be - * returned with the formatted [Date]s of the minimum and maximum dates respectively. - * Otherwise, the formatted name of the minimum [Date] will be returned. + * returned with the formatted [Date]s of the minimum and maximum dates respectively. + * Otherwise, the formatted name of the minimum [Date] will be returned. */ fun resolveDate(context: Context) = if (min != max) { @@ -149,9 +153,10 @@ class Date private constructor(private val tokens: List) : Comparable companion object { /** * Create a [Range] from the given list of [Date]s. + * * @param dates The [Date]s to use. * @return A [Range] based on the minimum and maximum [Date]s. If there are no [Date]s, - * null is returned. + * null is returned. */ fun from(dates: List): Range? { if (dates.isEmpty()) { @@ -186,6 +191,7 @@ class Date private constructor(private val tokens: List) : Comparable /** * Create a [Date] from a year component. + * * @param year The year component. * @return A new [Date] of the given component, or null if the component is invalid. */ @@ -204,38 +210,41 @@ class Date private constructor(private val tokens: List) : Comparable /** * Create a [Date] from a date component. + * * @param year The year component. * @param month The month component. * @param day The day component. * @return A new [Date] consisting of the given components. May have reduced precision if - * the components were partially invalid, and will be null if all components are invalid. + * the components were partially invalid, and will be null if all components are invalid. */ fun from(year: Int, month: Int, day: Int) = fromTokens(listOf(year, month, day)) /** * Create [Date] from a datetime component. + * * @param year The year component. * @param month The month component. * @param day The day component. * @param hour The hour component * @return A new [Date] consisting of the given components. May have reduced precision if - * the components were partially invalid, and will be null if all components are invalid. + * the components were partially invalid, and will be null if all components are invalid. */ fun from(year: Int, month: Int, day: Int, hour: Int, minute: Int) = fromTokens(listOf(year, month, day, hour, minute)) /** * Create a [Date] from a [String] timestamp. + * * @param timestamp The ISO-8601 timestamp to parse. Can have reduced precision. * @return A new [Date] consisting of the given components. May have reduced precision if - * the components were partially invalid, and will be null if all components are invalid or - * if the timestamp is invalid. + * the components were partially invalid, and will be null if all components are invalid + * or if the timestamp is invalid. */ fun from(timestamp: String): Date? { val tokens = - // Match the input with the timestamp regex. If there is no match, see if we can - // fall back to some kind of year value. - (ISO8601_REGEX.matchEntire(timestamp) + // Match the input with the timestamp regex. If there is no match, see if we can + // fall back to some kind of year value. + (ISO8601_REGEX.matchEntire(timestamp) ?: return timestamp.toIntOrNull()?.let(Companion::from)) .groupValues // Filter to the specific tokens we want and convert them to integer tokens. @@ -245,9 +254,10 @@ class Date private constructor(private val tokens: List) : Comparable /** * Create a [Date] from the given non-validated tokens. + * * @param tokens The tokens to use for each date component, in order of precision. * @return A new [Date] consisting of the given components. May have reduced precision if - * the components were partially invalid, and will be null if all components are invalid. + * the components were partially invalid, and will be null if all components are invalid. */ private fun fromTokens(tokens: List): Date? { val validated = mutableListOf() @@ -262,6 +272,7 @@ class Date private constructor(private val tokens: List) : Comparable /** * Validate a list of tokens provided by [src], and add the valid ones to [dst]. Will stop * as soon as an invalid token is found. + * * @param src The input tokens to validate. * @param dst The destination list to add valid tokens to. */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/Disc.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/Disc.kt index b38db06fb..e75e517d1 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/Disc.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/Disc.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * Disc.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,6 +22,7 @@ import org.oxycblt.auxio.list.Item /** * A disc identifier for a song. + * * @param number The disc number. * @param name The name of the disc group, if any. Null if not present. */ 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 469d21a7b..d6be65f67 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * MetadataModule.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 @@ -26,5 +27,6 @@ import dagger.hilt.components.SingletonComponent @InstallIn(SingletonComponent::class) interface MetadataModule { @Binds fun tagExtractor(tagExtractor: TagExtractorImpl): TagExtractor + @Binds fun tagWorkerFactory(taskFactory: TagWorkerImpl.Factory): TagWorker.Factory @Binds fun audioInfoProvider(audioInfoProvider: AudioInfoProviderImpl): AudioInfo.Provider } diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/ReleaseType.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/ReleaseType.kt index 2479444a9..a91966d56 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/ReleaseType.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/ReleaseType.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * ReleaseType.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 @@ -24,6 +25,7 @@ import org.oxycblt.auxio.R * * This class is derived from the MusicBrainz Release Group Type specification. It can be found at: * https://musicbrainz.org/doc/Release_Group/Type + * * @author Alexander Capehart (OxygenCobalt) */ sealed class ReleaseType { @@ -38,8 +40,9 @@ sealed class ReleaseType { /** * A plain album. + * * @param refinement A specification of what kind of performance this release is. If null, the - * release is considered "Plain". + * release is considered "Plain". */ data class Album(override val refinement: Refinement?) : ReleaseType() { override val stringRes: Int @@ -54,8 +57,9 @@ sealed class ReleaseType { /** * A "Extended Play", or EP. Usually a smaller release consisting of 4-5 songs. + * * @param refinement A specification of what kind of performance this release is. If null, the - * release is considered "Plain". + * release is considered "Plain". */ data class EP(override val refinement: Refinement?) : ReleaseType() { override val stringRes: Int @@ -70,8 +74,9 @@ sealed class ReleaseType { /** * A single. Usually a release consisting of 1-2 songs. + * * @param refinement A specification of what kind of performance this release is. If null, the - * release is considered "Plain". + * release is considered "Plain". */ data class Single(override val refinement: Refinement?) : ReleaseType() { override val stringRes: Int @@ -86,8 +91,9 @@ sealed class ReleaseType { /** * A compilation. Usually consists of many songs from a variety of artists. + * * @param refinement A specification of what kind of performance this release is. If null, the - * release is considered "Plain". + * release is considered "Plain". */ data class Compilation(override val refinement: Refinement?) : ReleaseType() { override val stringRes: Int @@ -149,9 +155,10 @@ sealed class ReleaseType { /** * Parse a [ReleaseType] from a string formatted with the MusicBrainz Release Group Type * specification. + * * @param types A list of values consisting of valid release type values. * @return A [ReleaseType] consisting of the given types, or null if the types were not - * valid. + * valid. */ fun parse(types: List): ReleaseType? { val primary = types.getOrNull(0) ?: return null @@ -170,10 +177,11 @@ sealed class ReleaseType { /** * Parse "secondary" types (i.e not [Album], [EP], or [Single]) from a string formatted with * the MusicBrainz Release Group Type specification. + * * @param index The index of the release type to parse. * @param convertRefinement Code to convert a [Refinement] into a [ReleaseType] - * corresponding to the callee's context. This is used in order to handle secondary times - * that are actually [Refinement]s. + * corresponding to the callee's context. This is used in order to handle secondary times + * that are actually [Refinement]s. * @return A [ReleaseType] corresponding to the secondary type found at that index. */ private inline fun List.parseSecondaryTypes( @@ -194,10 +202,11 @@ sealed class ReleaseType { /** * Parse "secondary" types (i.e not [Album], [EP], [Single]) that do not correspond to any * child values. + * * @param type The release type value to parse. * @param convertRefinement Code to convert a [Refinement] into a [ReleaseType] - * corresponding to the callee's context. This is used in order to handle secondary times - * that are actually [Refinement]s. + * corresponding to the callee's context. This is used in order to handle secondary times + * that are actually [Refinement]s. */ private inline fun parseSecondaryTypeImpl( type: String?, diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/Separators.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/Separators.kt index dbc63447c..cf42234f4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/Separators.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/Separators.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * Separators.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 @@ -19,6 +20,7 @@ package org.oxycblt.auxio.music.metadata /** * Defines the allowed separator characters that can be used to delimit multi-value tags. + * * @author Alexander Capehart (OxygenCobalt) */ object Separators { diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt index 05ac292e2..2acf872ba 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/SeparatorsDialog.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * SeparatorsDialog.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 @@ -33,6 +34,7 @@ import org.oxycblt.auxio.ui.ViewBindingDialogFragment /** * A [ViewBindingDialogFragment] that allows the user to configure the separator characters used to * split tags with multiple values. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt index 581ff63c7..e9ed62dee 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * TagExtractor.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 @@ -17,20 +18,11 @@ package org.oxycblt.auxio.music.metadata -import android.content.Context -import androidx.core.text.isDigitsOnly -import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.MetadataRetriever -import com.google.android.exoplayer2.source.DefaultMediaSourceFactory -import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.yield -import org.oxycblt.auxio.music.AudioOnlyExtractors import org.oxycblt.auxio.music.model.RawSong -import org.oxycblt.auxio.music.storage.toAudioUri -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW /** * The extractor that leverages ExoPlayer's [MetadataRetriever] API to parse metadata. This is the @@ -43,13 +35,14 @@ interface TagExtractor { /** * Extract the metadata of songs from [incompleteSongs] and send them to [completeSongs]. Will * terminate as soon as [incompleteSongs] is closed. + * * @param incompleteSongs A [Channel] of incomplete songs to process. * @param completeSongs A [Channel] to send completed songs to. */ suspend fun consume(incompleteSongs: Channel, completeSongs: Channel) } -class TagExtractorImpl @Inject constructor(@ApplicationContext private val context: Context) : +class TagExtractorImpl @Inject constructor(private val tagWorkerFactory: TagWorker.Factory) : TagExtractor { override suspend fun consume( incompleteSongs: Channel, @@ -57,22 +50,22 @@ class TagExtractorImpl @Inject constructor(@ApplicationContext private val conte ) { // We can parallelize MetadataRetriever Futures to work around it's speed issues, // producing similar throughput's to other kinds of manual metadata extraction. - val taskPool: Array = arrayOfNulls(TASK_CAPACITY) + val tagWorkerPool: Array = arrayOfNulls(TASK_CAPACITY) - for (song in incompleteSongs) { + for (incompleteRawSong in incompleteSongs) { spin@ while (true) { - for (i in taskPool.indices) { - val task = taskPool[i] - if (task != null) { - val finishedRawSong = task.get() - if (finishedRawSong != null) { - completeSongs.send(finishedRawSong) + for (i in tagWorkerPool.indices) { + val worker = tagWorkerPool[i] + if (worker != null) { + val completeRawSong = worker.poll() + if (completeRawSong != null) { + completeSongs.send(completeRawSong) yield() } else { continue } } - taskPool[i] = Task(context, song) + tagWorkerPool[i] = tagWorkerFactory.create(incompleteRawSong) break@spin } } @@ -80,13 +73,13 @@ class TagExtractorImpl @Inject constructor(@ApplicationContext private val conte do { var ongoingTasks = false - for (i in taskPool.indices) { - val task = taskPool[i] + for (i in tagWorkerPool.indices) { + val task = tagWorkerPool[i] if (task != null) { - val finishedRawSong = task.get() - if (finishedRawSong != null) { - completeSongs.send(finishedRawSong) - taskPool[i] = null + val completeRawSong = task.poll() + if (completeRawSong != null) { + completeSongs.send(completeRawSong) + tagWorkerPool[i] = null yield() } else { ongoingTasks = true @@ -102,216 +95,3 @@ class TagExtractorImpl @Inject constructor(@ApplicationContext private val conte const val TASK_CAPACITY = 8 } } - -/** - * Wraps a [TagExtractor] future and processes it into a [RawSong] when completed. - * @param context [Context] required to open the audio file. - * @param rawSong [RawSong] to process. - * @author Alexander Capehart (OxygenCobalt) - */ -private class Task(context: Context, private val rawSong: RawSong) { - // Note that we do not leverage future callbacks. This is because errors in the - // (highly fallible) extraction process will not bubble up to Indexer when a - // listener is used, instead crashing the app entirely. - private val future = - MetadataRetriever.retrieveMetadata( - DefaultMediaSourceFactory(context, AudioOnlyExtractors), - MediaItem.fromUri( - requireNotNull(rawSong.mediaStoreId) { "Invalid raw: No id" }.toAudioUri())) - - /** - * Try to get a completed song from this [Task], if it has finished processing. - * @return A [RawSong] instance if processing has completed, null otherwise. - */ - fun get(): RawSong? { - if (!future.isDone) { - // Not done yet, nothing to do. - return null - } - - val format = - try { - future.get()[0].getFormat(0) - } catch (e: Exception) { - logW("Unable to extract metadata for ${rawSong.name}") - logW(e.stackTraceToString()) - null - } - if (format == null) { - logD("Nothing could be extracted for ${rawSong.name}") - return rawSong - } - - val metadata = format.metadata - if (metadata != null) { - val textTags = TextTags(metadata) - populateWithId3v2(textTags.id3v2) - populateWithVorbis(textTags.vorbis) - } else { - logD("No metadata could be extracted for ${rawSong.name}") - } - - return rawSong - } - - /** - * Complete this instance's [RawSong] with ID3v2 Text Identification Frames. - * @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more - * values. - */ - private fun populateWithId3v2(textFrames: Map>) { - // Song - textFrames["TXXX:musicbrainz release track id"]?.let { rawSong.musicBrainzId = it.first() } - textFrames["TIT2"]?.let { rawSong.name = it.first() } - textFrames["TSOT"]?.let { rawSong.sortName = it.first() } - - // Track. - textFrames["TRCK"]?.run { first().parseId3v2PositionField() }?.let { rawSong.track = it } - - // Disc and it's subtitle name. - textFrames["TPOS"]?.run { first().parseId3v2PositionField() }?.let { rawSong.disc = it } - textFrames["TSST"]?.let { rawSong.subtitle = it.first() } - - // Dates are somewhat complicated, as not only did their semantics change from a flat year - // value in ID3v2.3 to a full ISO-8601 date in ID3v2.4, but there are also a variety of - // date types. - // Our hierarchy for dates is as such: - // 1. ID3v2.4 Original Date, as it resolves the "Released in X, Remastered in Y" issue - // 2. ID3v2.4 Recording Date, as it is the most common date type - // 3. ID3v2.4 Release Date, as it is the second most common date type - // 4. ID3v2.3 Original Date, as it is like #1 - // 5. ID3v2.3 Release Year, as it is the most common date type - (textFrames["TDOR"]?.run { Date.from(first()) } - ?: textFrames["TDRC"]?.run { Date.from(first()) } - ?: textFrames["TDRL"]?.run { Date.from(first()) } - ?: parseId3v23Date(textFrames)) - ?.let { rawSong.date = it } - - // Album - textFrames["TXXX:musicbrainz album id"]?.let { rawSong.albumMusicBrainzId = it.first() } - textFrames["TALB"]?.let { rawSong.albumName = it.first() } - textFrames["TSOA"]?.let { rawSong.albumSortName = it.first() } - (textFrames["TXXX:musicbrainz album type"] - ?: textFrames["TXXX:releasetype"] ?: textFrames["GRP1"]) - ?.let { rawSong.releaseTypes = it } - - // Artist - textFrames["TXXX:musicbrainz artist id"]?.let { rawSong.artistMusicBrainzIds = it } - (textFrames["TXXX:artists"] ?: textFrames["TPE1"])?.let { rawSong.artistNames = it } - (textFrames["TXXX:artists_sort"] ?: textFrames["TSOP"])?.let { - rawSong.artistSortNames = it - } - - // Album artist - textFrames["TXXX:musicbrainz album artist id"]?.let { - rawSong.albumArtistMusicBrainzIds = it - } - (textFrames["TXXX:albumartists"] ?: textFrames["TPE2"])?.let { - rawSong.albumArtistNames = it - } - (textFrames["TXXX:albumartists_sort"] ?: textFrames["TSO2"])?.let { - rawSong.albumArtistSortNames = it - } - - // Genre - textFrames["TCON"]?.let { rawSong.genreNames = it } - } - - /** - * Parses the ID3v2.3 timestamp specification into a [Date] from the given Text Identification - * Frames. - * @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more - * values. - * @return A [Date] of a year value from TORY/TYER, a month and day value from TDAT, and a - * hour/minute value from TIME. No second value is included. The latter two fields may not be - * included in they cannot be parsed. Will be null if a year value could not be parsed. - */ - private fun parseId3v23Date(textFrames: Map>): Date? { - // Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY - // is present. - val year = - textFrames["TORY"]?.run { first().toIntOrNull() } - ?: textFrames["TYER"]?.run { first().toIntOrNull() } ?: return null - - val tdat = textFrames["TDAT"] - return if (tdat != null && tdat.first().length == 4 && tdat.first().isDigitsOnly()) { - // TDAT frames consist of a 4-digit string where the first two digits are - // the month and the last two digits are the day. - val mm = tdat.first().substring(0..1).toInt() - val dd = tdat.first().substring(2..3).toInt() - - val time = textFrames["TIME"] - if (time != null && time.first().length == 4 && time.first().isDigitsOnly()) { - // TIME frames consist of a 4-digit string where the first two digits are - // the hour and the last two digits are the minutes. No second value is - // possible. - val hh = time.first().substring(0..1).toInt() - val mi = time.first().substring(2..3).toInt() - // Able to return a full date. - Date.from(year, mm, dd, hh, mi) - } else { - // Unable to parse time, just return a date - Date.from(year, mm, dd) - } - } else { - // Unable to parse month/day, just return a year - return Date.from(year) - } - } - - /** - * Complete this instance's [RawSong] with Vorbis comments. - * @param comments A mapping between vorbis comment names and one or more vorbis comment values. - */ - private fun populateWithVorbis(comments: Map>) { - // Song - comments["musicbrainz_releasetrackid"]?.let { rawSong.musicBrainzId = it.first() } - comments["title"]?.let { rawSong.name = it.first() } - comments["titlesort"]?.let { rawSong.sortName = it.first() } - - // Track. - parseVorbisPositionField( - comments["tracknumber"]?.first(), - (comments["totaltracks"] ?: comments["tracktotal"] ?: comments["trackc"])?.first()) - ?.let { rawSong.track = it } - - // Disc and it's subtitle name. - parseVorbisPositionField( - comments["discnumber"]?.first(), - (comments["totaldiscs"] ?: comments["disctotal"] ?: comments["discc"])?.first()) - ?.let { rawSong.disc = it } - comments["discsubtitle"]?.let { rawSong.subtitle = it.first() } - - // Vorbis dates are less complicated, but there are still several types - // Our hierarchy for dates is as such: - // 1. Original Date, as it solves the "Released in X, Remastered in Y" issue - // 2. Date, as it is the most common date type - // 3. Year, as old vorbis tags tended to use this (I know this because it's the only - // date tag that android supports, so it must be 15 years old or more!) - (comments["originaldate"]?.run { Date.from(first()) } - ?: comments["date"]?.run { Date.from(first()) } - ?: comments["year"]?.run { Date.from(first()) }) - ?.let { rawSong.date = it } - - // Album - comments["musicbrainz_albumid"]?.let { rawSong.albumMusicBrainzId = it.first() } - comments["album"]?.let { rawSong.albumName = it.first() } - comments["albumsort"]?.let { rawSong.albumSortName = it.first() } - comments["releasetype"]?.let { rawSong.releaseTypes = it } - - // Artist - comments["musicbrainz_artistid"]?.let { rawSong.artistMusicBrainzIds = it } - (comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it } - (comments["artists_sort"] ?: comments["artistsort"])?.let { rawSong.artistSortNames = it } - - // Album artist - comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = it } - (comments["albumartists"] ?: comments["albumartist"])?.let { rawSong.albumArtistNames = it } - (comments["albumartists_sort"] ?: comments["albumartistsort"])?.let { - rawSong.albumArtistSortNames = it - } - - // Genre - comments["genre"]?.let { rawSong.genreNames = it } - } -} 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 91d59e139..9dd1bbb36 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * TagUtil.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 @@ -26,6 +27,7 @@ import org.oxycblt.auxio.util.nonZeroOrNull * Parse a multi-value tag based on the user configuration. If the value is already composed of more * than one value, nothing is done. Otherwise, this function will attempt to split it based on the * user's separator preferences. + * * @param settings [MusicSettings] required to obtain user separator configuration. * @return A new list of one or more [String]s. */ @@ -40,6 +42,7 @@ fun List.parseMultiValue(settings: MusicSettings) = /** * Split a [String] by the given selector, automatically handling escaped characters that satisfy * the selector. + * * @param selector A block that determines if the string should be split at a given character. * @return One or more [String]s split by the selector. */ @@ -83,19 +86,22 @@ inline fun String.splitEscaped(selector: (Char) -> Boolean): List { /** * Fix trailing whitespace or blank contents in a [String]. + * * @return A string with trailing whitespace remove,d or null if the [String] was all whitespace or - * empty. + * empty. */ fun String.correctWhitespace() = trim().ifBlank { null } /** * Fix trailing whitespace or blank contents within a list of [String]s. + * * @return A list of non-blank strings with trailing whitespace removed. */ fun List.correctWhitespace() = mapNotNull { it.correctWhitespace() } /** * Attempt to parse a string by the user's separator preferences. + * * @param settings [MusicSettings] required to obtain user separator configuration. * @return A list of one or more [String]s that were split up by the user-defined separators. */ @@ -109,9 +115,11 @@ private fun String.maybeParseBySeparators(settings: MusicSettings): List /** * Parse an ID3v2-style position + total [String] field. These fields consist of a number and an * (optional) total value delimited by a /. + * * @return The position value extracted from the string field, or null if: * - The position could not be parsed * - The position was zeroed AND the total value was not present/zeroed + * * @see transformPositionField */ fun String.parseId3v2PositionField() = @@ -122,11 +130,13 @@ fun String.parseId3v2PositionField() = /** * Parse a vorbis-style position + total field. These fields consist of two fields for the position * and total numbers. + * * @param pos The position value, or null if not present. * @param total The total value, if not present. * @return The position value extracted from the field, or null if: * - The position could not be parsed * - The position was zeroed AND the total value was not present/zeroed + * * @see transformPositionField */ fun parseVorbisPositionField(pos: String?, total: String?) = @@ -134,6 +144,7 @@ fun parseVorbisPositionField(pos: String?, total: String?) = /** * Transform a raw position + total field into a position a way that tolerates placeholder values. + * * @param pos The position value, or null if not present. * @param total The total value, if not present. * @return The position value extracted from the field, or null if: @@ -151,6 +162,7 @@ fun transformPositionField(pos: Int?, total: Int?) = * Parse a multi-value genre name using ID3 rules. This will convert any ID3v1 integer * representations of genre fields into their named counterparts, and split up singular ID3v2-style * integer genre fields into one or more genres. + * * @param settings [MusicSettings] required to obtain user separator configuration. * @return A list of one or more genre names.. */ @@ -164,6 +176,7 @@ fun List.parseId3GenreNames(settings: MusicSettings) = /** * Parse a single ID3v1/ID3v2 integer genre field into their named representations. + * * @param settings [MusicSettings] required to obtain user separator configuration. * @return A list of one or more genre names. */ @@ -172,8 +185,9 @@ private fun String.parseId3MultiValueGenre(settings: MusicSettings) = /** * Parse an ID3v1 integer genre field. + * * @return A named genre if the field is a valid integer, "Cover" or "Remix" if the field is - * "CR"/"RX" respectively, and nothing if the field is not a valid ID3v1 integer genre. + * "CR"/"RX" respectively, and nothing if the field is not a valid ID3v1 integer genre. */ private fun String.parseId3v1Genre(): String? { // ID3v1 genres are a plain integer value without formatting, so in that case @@ -200,6 +214,7 @@ private val ID3V2_GENRE_RE = Regex("((?:\\((\\d+|RX|CR)\\))*)(.+)?") /** * Parse an ID3v2 integer genre field, which has support for multiple genre values and combined * named/integer genres. + * * @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? { diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt new file mode 100644 index 000000000..2cf328a92 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagWorker.kt @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2023 Auxio Project + * TagWorker.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.music.metadata + +import androidx.core.text.isDigitsOnly +import com.google.android.exoplayer2.MediaItem +import com.google.android.exoplayer2.MetadataRetriever +import com.google.android.exoplayer2.source.MediaSource +import com.google.android.exoplayer2.source.TrackGroupArray +import java.util.concurrent.Future +import javax.inject.Inject +import org.oxycblt.auxio.music.model.RawSong +import org.oxycblt.auxio.music.storage.toAudioUri +import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.logW + +/** + * An processing abstraction over the [MetadataRetriever] and [TextTags] workflow that operates on + * [RawSong] instances. + * + * @author Alexander Capehart (OxygenCobalt) + */ +interface TagWorker { + /** + * Poll to see if this worker is done processing. + * + * @return A completed [RawSong] if done, null otherwise. + */ + fun poll(): RawSong? + + /** Factory for new [TagWorker] jobs. */ + interface Factory { + /** + * Create a new [TagWorker] to complete the given [RawSong]. + * + * @param rawSong The [RawSong] to assign a new [TagWorker] to. + * @return A new [TagWorker] wrapping the given [RawSong]. + */ + fun create(rawSong: RawSong): TagWorker + } +} + +class TagWorkerImpl +private constructor(private val rawSong: RawSong, private val future: Future) : + TagWorker { + /** + * Try to get a completed song from this [TagWorker], if it has finished processing. + * + * @return A [RawSong] instance if processing has completed, null otherwise. + */ + override fun poll(): RawSong? { + if (!future.isDone) { + // Not done yet, nothing to do. + return null + } + + val format = + try { + future.get()[0].getFormat(0) + } catch (e: Exception) { + logW("Unable to extract metadata for ${rawSong.name}") + logW(e.stackTraceToString()) + null + } + if (format == null) { + logD("Nothing could be extracted for ${rawSong.name}") + return rawSong + } + + val metadata = format.metadata + if (metadata != null) { + val textTags = TextTags(metadata) + populateWithId3v2(textTags.id3v2) + populateWithVorbis(textTags.vorbis) + } else { + logD("No metadata could be extracted for ${rawSong.name}") + } + + return rawSong + } + + /** + * Complete this instance's [RawSong] with ID3v2 Text Identification Frames. + * + * @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more + * values. + */ + private fun populateWithId3v2(textFrames: Map>) { + // Song + textFrames["TXXX:musicbrainz release track id"]?.let { rawSong.musicBrainzId = it.first() } + textFrames["TIT2"]?.let { rawSong.name = it.first() } + textFrames["TSOT"]?.let { rawSong.sortName = it.first() } + + // Track. + textFrames["TRCK"]?.run { first().parseId3v2PositionField() }?.let { rawSong.track = it } + + // Disc and it's subtitle name. + textFrames["TPOS"]?.run { first().parseId3v2PositionField() }?.let { rawSong.disc = it } + textFrames["TSST"]?.let { rawSong.subtitle = it.first() } + + // Dates are somewhat complicated, as not only did their semantics change from a flat year + // value in ID3v2.3 to a full ISO-8601 date in ID3v2.4, but there are also a variety of + // date types. + // Our hierarchy for dates is as such: + // 1. ID3v2.4 Original Date, as it resolves the "Released in X, Remastered in Y" issue + // 2. ID3v2.4 Recording Date, as it is the most common date type + // 3. ID3v2.4 Release Date, as it is the second most common date type + // 4. ID3v2.3 Original Date, as it is like #1 + // 5. ID3v2.3 Release Year, as it is the most common date type + (textFrames["TDOR"]?.run { Date.from(first()) } + ?: textFrames["TDRC"]?.run { Date.from(first()) } + ?: textFrames["TDRL"]?.run { Date.from(first()) } + ?: parseId3v23Date(textFrames)) + ?.let { rawSong.date = it } + + // Album + textFrames["TXXX:musicbrainz album id"]?.let { rawSong.albumMusicBrainzId = it.first() } + textFrames["TALB"]?.let { rawSong.albumName = it.first() } + textFrames["TSOA"]?.let { rawSong.albumSortName = it.first() } + (textFrames["TXXX:musicbrainz album type"] + ?: textFrames["TXXX:releasetype"] ?: textFrames["GRP1"]) + ?.let { rawSong.releaseTypes = it } + + // Artist + textFrames["TXXX:musicbrainz artist id"]?.let { rawSong.artistMusicBrainzIds = it } + (textFrames["TXXX:artists"] ?: textFrames["TPE1"])?.let { rawSong.artistNames = it } + (textFrames["TXXX:artists_sort"] ?: textFrames["TSOP"])?.let { + rawSong.artistSortNames = it + } + + // Album artist + textFrames["TXXX:musicbrainz album artist id"]?.let { + rawSong.albumArtistMusicBrainzIds = it + } + (textFrames["TXXX:albumartists"] ?: textFrames["TPE2"])?.let { + rawSong.albumArtistNames = it + } + (textFrames["TXXX:albumartists_sort"] ?: textFrames["TSO2"])?.let { + rawSong.albumArtistSortNames = it + } + + // Genre + textFrames["TCON"]?.let { rawSong.genreNames = it } + + // Compilation Flag + (textFrames["TCMP"] + ?: textFrames["TXXX:compilation"] ?: textFrames["TXXX:itunescompilation"]) + ?.let { + if (it.size != 1 || it[0] != "1") return@let + rawSong.albumArtistNames = + rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS } + rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES } + } + } + + /** + * Parses the ID3v2.3 timestamp specification into a [Date] from the given Text Identification + * Frames. + * + * @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more + * values. + * @return A [Date] of a year value from TORY/TYER, a month and day value from TDAT, and a + * hour/minute value from TIME. No second value is included. The latter two fields may not be + * included in they cannot be parsed. Will be null if a year value could not be parsed. + */ + private fun parseId3v23Date(textFrames: Map>): Date? { + // Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY + // is present. + val year = + textFrames["TORY"]?.run { first().toIntOrNull() } + ?: textFrames["TYER"]?.run { first().toIntOrNull() } ?: return null + + val tdat = textFrames["TDAT"] + return if (tdat != null && tdat.first().length == 4 && tdat.first().isDigitsOnly()) { + // TDAT frames consist of a 4-digit string where the first two digits are + // the month and the last two digits are the day. + val mm = tdat.first().substring(0..1).toInt() + val dd = tdat.first().substring(2..3).toInt() + + val time = textFrames["TIME"] + if (time != null && time.first().length == 4 && time.first().isDigitsOnly()) { + // TIME frames consist of a 4-digit string where the first two digits are + // the hour and the last two digits are the minutes. No second value is + // possible. + val hh = time.first().substring(0..1).toInt() + val mi = time.first().substring(2..3).toInt() + // Able to return a full date. + Date.from(year, mm, dd, hh, mi) + } else { + // Unable to parse time, just return a date + Date.from(year, mm, dd) + } + } else { + // Unable to parse month/day, just return a year + return Date.from(year) + } + } + + /** + * Complete this instance's [RawSong] with Vorbis comments. + * + * @param comments A mapping between vorbis comment names and one or more vorbis comment values. + */ + private fun populateWithVorbis(comments: Map>) { + // Song + comments["musicbrainz_releasetrackid"]?.let { rawSong.musicBrainzId = it.first() } + comments["title"]?.let { rawSong.name = it.first() } + comments["titlesort"]?.let { rawSong.sortName = it.first() } + + // Track. + parseVorbisPositionField( + comments["tracknumber"]?.first(), + (comments["totaltracks"] ?: comments["tracktotal"] ?: comments["trackc"])?.first()) + ?.let { rawSong.track = it } + + // Disc and it's subtitle name. + parseVorbisPositionField( + comments["discnumber"]?.first(), + (comments["totaldiscs"] ?: comments["disctotal"] ?: comments["discc"])?.first()) + ?.let { rawSong.disc = it } + comments["discsubtitle"]?.let { rawSong.subtitle = it.first() } + + // Vorbis dates are less complicated, but there are still several types + // Our hierarchy for dates is as such: + // 1. Original Date, as it solves the "Released in X, Remastered in Y" issue + // 2. Date, as it is the most common date type + // 3. Year, as old vorbis tags tended to use this (I know this because it's the only + // date tag that android supports, so it must be 15 years old or more!) + (comments["originaldate"]?.run { Date.from(first()) } + ?: comments["date"]?.run { Date.from(first()) } + ?: comments["year"]?.run { Date.from(first()) }) + ?.let { rawSong.date = it } + + // Album + comments["musicbrainz_albumid"]?.let { rawSong.albumMusicBrainzId = it.first() } + comments["album"]?.let { rawSong.albumName = it.first() } + comments["albumsort"]?.let { rawSong.albumSortName = it.first() } + comments["releasetype"]?.let { rawSong.releaseTypes = it } + + // Artist + comments["musicbrainz_artistid"]?.let { rawSong.artistMusicBrainzIds = it } + (comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it } + (comments["artists_sort"] ?: comments["artistsort"])?.let { rawSong.artistSortNames = it } + + // Album artist + comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = it } + (comments["albumartists"] ?: comments["albumartist"])?.let { rawSong.albumArtistNames = it } + (comments["albumartists_sort"] ?: comments["albumartistsort"])?.let { + rawSong.albumArtistSortNames = it + } + + // Genre + comments["genre"]?.let { rawSong.genreNames = it } + + // Compilation Flag + (comments["compilation"] ?: comments["itunescompilation"])?.let { + if (it.size != 1 || it[0] != "1") return@let + rawSong.albumArtistNames = + rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS } + rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES } + } + } + + class Factory @Inject constructor(private val mediaSourceFactory: MediaSource.Factory) : + TagWorker.Factory { + override fun create(rawSong: RawSong) = + // Note that we do not leverage future callbacks. This is because errors in the + // (highly fallible) extraction process will not bubble up to Indexer when a + // listener is used, instead crashing the app entirely. + TagWorkerImpl( + rawSong, + MetadataRetriever.retrieveMetadata( + mediaSourceFactory, + MediaItem.fromUri( + requireNotNull(rawSong.mediaStoreId) { "Invalid raw: No id" } + .toAudioUri()))) + } + + private companion object { + val COMPILATION_ALBUM_ARTISTS = listOf("Various Artists") + val COMPILATION_RELEASE_TYPES = listOf("compilation") + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TextTags.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TextTags.kt index de3c28c75..9b486c623 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TextTags.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TextTags.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * TextTags.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 @@ -24,6 +25,7 @@ import com.google.android.exoplayer2.metadata.vorbis.VorbisComment /** * Processing wrapper for [Metadata] that allows organized access to text-based audio tags. + * * @param metadata The [Metadata] to wrap. * @author Alexander Capehart (OxygenCobalt) */ @@ -79,8 +81,9 @@ class TextTags(metadata: Metadata) { /** * Copies and sanitizes a possibly invalid string outputted from ExoPlayer. + * * @return A new string allocated in a memory-safe manner with any UTF-8 errors replaced with - * the Unicode replacement byte sequence. + * the Unicode replacement byte sequence. */ private fun String.sanitize() = String(encodeToByteArray()) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/model/Library.kt b/app/src/main/java/org/oxycblt/auxio/music/model/Library.kt index 54c99a084..4f3b217c8 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/model/Library.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/model/Library.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * Library.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 @@ -47,14 +48,16 @@ interface Library { /** * Finds a [Music] item [T] in the library by it's [Music.UID]. + * * @param uid The [Music.UID] to search for. * @return The [T] corresponding to the given [Music.UID], or null if nothing could be found or - * the [Music.UID] did not correspond to a [T]. + * the [Music.UID] did not correspond to a [T]. */ fun find(uid: Music.UID): T? /** * Convert a [Song] from an another library into a [Song] in this [Library]. + * * @param song The [Song] to convert. * @return The analogous [Song] in this [Library], or null if it does not exist. */ @@ -62,6 +65,7 @@ interface Library { /** * Convert a [MusicParent] from an another library into a [MusicParent] in this [Library]. + * * @param parent The [MusicParent] to convert. * @return The analogous [Album] in this [Library], or null if it does not exist. */ @@ -69,6 +73,7 @@ interface Library { /** * Find a [Song] instance corresponding to the given Intent.ACTION_VIEW [Uri]. + * * @param context [Context] required to analyze the [Uri]. * @param uri [Uri] to search for. * @return A [Song] corresponding to the given [Uri], or null if one could not be found. @@ -78,6 +83,7 @@ interface Library { companion object { /** * Create an instance of [Library]. + * * @param rawSongs [RawSong]s to create the library out of. * @param settings [MusicSettings] required. */ @@ -117,9 +123,10 @@ private class LibraryImpl(rawSongs: List, settings: MusicSettings) : Li /** * Finds a [Music] item [T] in the library by it's [Music.UID]. + * * @param uid The [Music.UID] to search for. * @return The [T] corresponding to the given [Music.UID], or null if nothing could be found or - * the [Music.UID] did not correspond to a [T]. + * the [Music.UID] did not correspond to a [T]. */ @Suppress("UNCHECKED_CAST") override fun find(uid: Music.UID) = uidMap[uid] as? T @@ -130,21 +137,22 @@ private class LibraryImpl(rawSongs: List, settings: MusicSettings) : Li override fun findSongForUri(context: Context, uri: Uri) = context.contentResolverSafe.useQuery( uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) { cursor -> - cursor.moveToFirst() - // We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a - // song. Do what we can to hopefully find the song the user wanted to open. - val displayName = - cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) - val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)) - songs.find { it.path.name == displayName && it.size == size } - } + cursor.moveToFirst() + // We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a + // song. Do what we can to hopefully find the song the user wanted to open. + val displayName = + cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) + val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)) + songs.find { it.path.name == displayName && it.size == size } + } /** * Build a list [SongImpl]s from the given [RawSong]. + * * @param rawSongs The [RawSong]s to build the [SongImpl]s from. * @param settings [MusicSettings] to obtain user parsing configuration. * @return A sorted list of [SongImpl]s derived from the [RawSong] that should be suitable for - * grouping. + * grouping. */ private fun buildSongs(rawSongs: List, settings: MusicSettings) = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING) @@ -152,11 +160,12 @@ private class LibraryImpl(rawSongs: List, settings: MusicSettings) : Li /** * Build a list of [Album]s from the given [Song]s. + * * @param songs The [Song]s to build [Album]s from. These will be linked with their respective - * [Album]s when created. + * [Album]s when created. * @param settings [MusicSettings] to obtain user parsing configuration. * @return A non-empty list of [Album]s. These [Album]s will be incomplete and must be linked - * with parent [Artist] instances in order to be usable. + * with parent [Artist] instances in order to be usable. */ private fun buildAlbums(songs: List, settings: MusicSettings): List { // Group songs by their singular raw album, then map the raw instances and their @@ -171,15 +180,16 @@ private class LibraryImpl(rawSongs: List, settings: MusicSettings) : Li * Group up [Song]s and [Album]s into [Artist] instances. Both of these items are required as * they group into [Artist] instances much differently, with [Song]s being grouped primarily by * artist names, and [Album]s being grouped primarily by album artist names. + * * @param songs The [Song]s to build [Artist]s from. One [Song] can result in the creation of - * one or more [Artist] instances. These will be linked with their respective [Artist]s when - * created. + * one or more [Artist] instances. These will be linked with their respective [Artist]s when + * created. * @param albums The [Album]s to build [Artist]s from. One [Album] can result in the creation of - * one or more [Artist] instances. These will be linked with their respective [Artist]s when - * created. + * one or more [Artist] instances. These will be linked with their respective [Artist]s when + * created. * @param settings [MusicSettings] to obtain user parsing configuration. * @return A non-empty list of [Artist]s. These [Artist]s will consist of the combined groupings - * of [Song]s and [Album]s. + * of [Song]s and [Album]s. */ private fun buildArtists( songs: List, @@ -210,9 +220,10 @@ private class LibraryImpl(rawSongs: List, settings: MusicSettings) : Li /** * Group up [Song]s into [Genre] instances. + * * @param [songs] The [Song]s to build [Genre]s from. One [Song] can result in the creation of - * one or more [Genre] instances. These will be linked with their respective [Genre]s when - * created. + * one or more [Genre] instances. These will be linked with their respective [Genre]s when + * created. * @param settings [MusicSettings] to obtain user parsing configuration. * @return A non-empty list of [Genre]s. */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/model/MusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/model/MusicImpl.kt index 1bf87f5bf..682c011fe 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/model/MusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/model/MusicImpl.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * MusicImpl.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 @@ -20,17 +21,9 @@ package org.oxycblt.auxio.music.model import android.content.Context import androidx.annotation.VisibleForTesting import java.security.MessageDigest -import java.text.CollationKey -import java.text.Collator import org.oxycblt.auxio.R import org.oxycblt.auxio.list.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.Song +import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.metadata.Date import org.oxycblt.auxio.music.metadata.Disc import org.oxycblt.auxio.music.metadata.ReleaseType @@ -46,14 +39,15 @@ import org.oxycblt.auxio.util.unlikelyToBeNull /** * Library-backed implementation of [Song]. + * * @param rawSong The [RawSong] to derive the member data from. * @param musicSettings [MusicSettings] to for user parsing configuration. * @author Alexander Capehart (OxygenCobalt) */ class SongImpl(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) } + // 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) { // 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 @@ -70,7 +64,7 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song { } override val rawName = requireNotNull(rawSong.name) { "Invalid raw: No title" } override val rawSortName = rawSong.sortName - override val collationKey = makeCollationKey(musicSettings) + override val sortName = SortName((rawSortName ?: rawName), musicSettings) override fun resolveName(context: Context) = rawName override val track = rawSong.track @@ -164,6 +158,7 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song { /** * Links this [Song] with a parent [Album]. + * * @param album The parent [Album] to link to. */ fun link(album: AlbumImpl) { @@ -172,6 +167,7 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song { /** * Links this [Song] with a parent [Artist]. + * * @param artist The parent [Artist] to link to. */ fun link(artist: ArtistImpl) { @@ -180,6 +176,7 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song { /** * Links this [Song] with a parent [Genre]. + * * @param genre The parent [Genre] to link to. */ fun link(genre: GenreImpl) { @@ -188,6 +185,7 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song { /** * Perform final validation and organization on this instance. + * * @return This instance upcasted to [Song]. */ fun finalize(): Song { @@ -218,10 +216,11 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song { /** * Library-backed implementation of [Album]. + * * @param rawAlbum The [RawAlbum] to derive the member data from. * @param musicSettings [MusicSettings] to for user parsing configuration. * @param songs The [Song]s that are a part of this [Album]. These items will be linked to this - * [Album]. + * [Album]. * @author Alexander Capehart (OxygenCobalt) */ class AlbumImpl( @@ -230,8 +229,8 @@ class AlbumImpl( override val songs: List ) : Album { 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) } + // 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) { // 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 @@ -241,7 +240,7 @@ class AlbumImpl( } override val rawName = rawAlbum.name override val rawSortName = rawAlbum.sortName - override val collationKey = makeCollationKey(musicSettings) + override val sortName = SortName((rawSortName ?: rawName), musicSettings) override fun resolveName(context: Context) = rawName override val dates = Date.Range.from(songs.mapNotNull { it.date }) @@ -286,6 +285,7 @@ class AlbumImpl( /** * Links this [Album] with a parent [Artist]. + * * @param artist The parent [Artist] to link to. */ fun link(artist: ArtistImpl) { @@ -294,6 +294,7 @@ class AlbumImpl( /** * Perform final validation and organization on this instance. + * * @return This instance upcasted to [Album]. */ fun finalize(): Album { @@ -313,11 +314,12 @@ class AlbumImpl( /** * Library-backed implementation of [Artist]. + * * @param rawArtist The [RawArtist] to derive the member data from. * @param musicSettings [MusicSettings] to for user parsing configuration. * @param songAlbums A list of the [Song]s and [Album]s that are a part of this [Artist] , either - * through artist or album artist tags. Providing [Song]s to the artist is optional. These instances - * will be linked to this [Artist]. + * through artist or album artist tags. Providing [Song]s to the artist is optional. These + * instances will be linked to this [Artist]. * @author Alexander Capehart (OxygenCobalt) */ class ArtistImpl( @@ -326,12 +328,12 @@ class ArtistImpl( songAlbums: List ) : Artist { 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) } + // 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) } override val rawName = rawArtist.name override val rawSortName = rawArtist.sortName - override val collationKey = makeCollationKey(musicSettings) + override val sortName = (rawSortName ?: rawName)?.let { SortName(it, musicSettings) } override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_artist) override val songs: List @@ -379,14 +381,16 @@ class ArtistImpl( * Returns the original position of this [Artist]'s [RawArtist] within the given [RawArtist] * list. This can be used to create a consistent ordering within child [Artist] lists based on * the original tag order. + * * @param rawArtists The [RawArtist] instances to check. It is assumed that this [Artist]'s - * [RawArtist] will be within the list. + * [RawArtist] will be within the list. * @return The index of the [Artist]'s [RawArtist] within the list. */ fun getOriginalPositionIn(rawArtists: List) = rawArtists.indexOf(rawArtist) /** * Perform final validation and organization on this instance. + * * @return This instance upcasted to [Artist]. */ fun finalize(): Artist { @@ -400,6 +404,7 @@ class ArtistImpl( } /** * Library-backed implementation of [Genre]. + * * @param rawGenre [RawGenre] to derive the member data from. * @param musicSettings [MusicSettings] to for user parsing configuration. * @param songs Child [SongImpl]s of this instance. @@ -413,7 +418,7 @@ class GenreImpl( override val uid = Music.UID.auxio(MusicMode.GENRES) { update(rawGenre.name) } override val rawName = rawGenre.name override val rawSortName = rawName - override val collationKey = makeCollationKey(musicSettings) + override val sortName = (rawSortName ?: rawName)?.let { SortName(it, musicSettings) } override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_genre) override val albums: List @@ -450,14 +455,16 @@ class GenreImpl( * Returns the original position of this [Genre]'s [RawGenre] within the given [RawGenre] list. * This can be used to create a consistent ordering within child [Genre] lists based on the * original tag order. + * * @param rawGenres The [RawGenre] instances to check. It is assumed that this [Genre] 's - * [RawGenre] will be within the list. + * [RawGenre] will be within the list. * @return The index of the [Genre]'s [RawGenre] within the list. */ fun getOriginalPositionIn(rawGenres: List) = rawGenres.indexOf(rawGenre) /** * Perform final validation and organization on this instance. + * * @return This instance upcasted to [Genre]. */ fun finalize(): Music { @@ -468,6 +475,7 @@ class GenreImpl( /** * Update a [MessageDigest] with a lowercase [String]. + * * @param string The [String] to hash. If null, it will not be hashed. */ @VisibleForTesting @@ -481,6 +489,7 @@ fun MessageDigest.update(string: String?) { /** * Update a [MessageDigest] with the string representation of a [Date]. + * * @param date The [Date] to hash. If null, nothing will be done. */ @VisibleForTesting @@ -494,6 +503,7 @@ fun MessageDigest.update(date: Date?) { /** * Update a [MessageDigest] with the lowercase versions of all of the input [String]s. + * * @param strings The [String]s to hash. If a [String] is null, it will not be hashed. */ @VisibleForTesting @@ -503,6 +513,7 @@ fun MessageDigest.update(strings: List) { /** * Update a [MessageDigest] with the little-endian bytes of a [Int]. + * * @param n The [Int] to write. If null, nothing will be done. */ @VisibleForTesting @@ -513,30 +524,3 @@ fun MessageDigest.update(n: Int?) { update(0) } } - -/** Cached collator instance re-used with [makeCollationKey]. */ -private val COLLATOR: Collator = Collator.getInstance().apply { strength = Collator.PRIMARY } - -/** - * Provided implementation to create a [CollationKey] in the way described by [Music.collationKey]. - * This should be used in all overrides of all [CollationKey]. - * @param musicSettings [MusicSettings] required for user parsing configuration. - * @return A [CollationKey] that follows the specification described by [Music.collationKey]. - */ -private fun Music.makeCollationKey(musicSettings: MusicSettings): CollationKey? { - var sortName = (rawSortName ?: rawName) ?: return null - - if (musicSettings.automaticSortNames) { - sortName = - sortName.run { - when { - length > 5 && startsWith("the ", ignoreCase = true) -> substring(4) - length > 4 && startsWith("an ", ignoreCase = true) -> substring(3) - length > 3 && startsWith("a ", ignoreCase = true) -> substring(2) - else -> this - } - } - } - - return COLLATOR.getCollationKey(sortName) -} diff --git a/app/src/main/java/org/oxycblt/auxio/music/model/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/model/RawMusic.kt index f532266c7..fa2042a60 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/model/RawMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/model/RawMusic.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * RawMusic.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 @@ -24,6 +25,7 @@ import org.oxycblt.auxio.music.storage.Directory /** * Raw information about a [SongImpl] obtained from the filesystem/Extractor instances. + * * @author Alexander Capehart (OxygenCobalt) */ class RawSong( @@ -88,6 +90,7 @@ class RawSong( /** * Raw information about an [AlbumImpl] obtained from the component [SongImpl] instances. + * * @author Alexander Capehart (OxygenCobalt) */ class RawAlbum( @@ -134,6 +137,7 @@ class RawAlbum( /** * Raw information about an [ArtistImpl] obtained from the component [SongImpl] and [AlbumImpl] * instances. + * * @author Alexander Capehart (OxygenCobalt) */ class RawArtist( @@ -175,6 +179,7 @@ class RawArtist( /** * Raw information about a [GenreImpl] obtained from the component [SongImpl] instances. + * * @author Alexander Capehart (OxygenCobalt) */ class RawGenre( diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt index dcdedc0ef..7c0117968 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * DirectoryAdapter.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 @@ -27,6 +28,7 @@ import org.oxycblt.auxio.util.inflater /** * [RecyclerView.Adapter] that manages a list of [Directory] instances. + * * @param listener A [DirectoryAdapter.Listener] to bind interactions to. * @author Alexander Capehart (OxygenCobalt) */ @@ -48,6 +50,7 @@ class DirectoryAdapter(private val listener: Listener) : /** * Add a [Directory] to the end of the list. + * * @param dir The [Directory] to add. */ fun add(dir: Directory) { @@ -61,6 +64,7 @@ class DirectoryAdapter(private val listener: Listener) : /** * Add a list of [Directory] instances to the end of the list. + * * @param dirs The [Directory instances to add. */ fun addAll(dirs: List) { @@ -71,6 +75,7 @@ class DirectoryAdapter(private val listener: Listener) : /** * Remove a [Directory] from the list. + * * @param dir The [Directory] to remove. Must exist in the list. */ fun remove(dir: Directory) { @@ -87,12 +92,14 @@ class DirectoryAdapter(private val listener: Listener) : /** * A [RecyclerView.Recycler] that displays a [Directory]. Use [from] to create an instance. + * * @author Alexander Capehart (OxygenCobalt) */ class MusicDirViewHolder private constructor(private val binding: ItemMusicDirBinding) : DialogRecyclerView.ViewHolder(binding.root) { /** * Bind new data to this instance. + * * @param dir The new [Directory] to bind. * @param listener A [DirectoryAdapter.Listener] to bind interactions to. */ @@ -104,6 +111,7 @@ class MusicDirViewHolder private constructor(private val binding: ItemMusicDirBi companion object { /** * Create a new instance. + * * @param parent The parent to inflate this instance from. * @return A new instance. */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/Filesystem.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/Filesystem.kt index ca0cdee6c..83369efd4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/Filesystem.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/Filesystem.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * Filesystem.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 @@ -28,6 +29,7 @@ import org.oxycblt.auxio.R /** * A full absolute path to a file. Only intended for display purposes. For accessing files, URIs are * preferred in all cases due to scoped storage limitations. + * * @param name The name of the file. * @param parent The parent [Directory] of the file. * @author Alexander Capehart (OxygenCobalt) @@ -36,6 +38,7 @@ data class Path(val name: String, val parent: Directory) /** * A volume-aware relative path to a directory. + * * @param volume The [StorageVolume] that the [Directory] is contained in. * @param relativePath The relative path from within the [StorageVolume] to the [Directory]. * @author Alexander Capehart (OxygenCobalt) @@ -43,6 +46,7 @@ data class Path(val name: String, val parent: Directory) class Directory private constructor(val volume: StorageVolume, val relativePath: String) { /** * Resolve the [Directory] instance into a human-readable path name. + * * @param context [Context] required to obtain volume descriptions. * @return A human-readable path. * @see StorageVolume.getDescription @@ -55,8 +59,9 @@ class Directory private constructor(val volume: StorageVolume, val relativePath: * violation of the document tree URI contract, but it's also the only one can sensibly work * with these uris in the UI, and it doesn't exactly matter since we never write or read to * directory. + * * @return A URI [String] abiding by the document tree specification, or null if the [Directory] - * is not valid. + * is not valid. */ fun toDocumentTreeUri() = // Document tree URIs consist of a prefixed volume name followed by a relative path. @@ -84,9 +89,10 @@ class Directory private constructor(val volume: StorageVolume, val relativePath: /** * Create a new directory instance from the given components. + * * @param volume The [StorageVolume] that the [Directory] is contained in. * @param relativePath The relative path from within the [StorageVolume] to the [Directory]. - * Will be stripped of any trailing separators for a consistent internal representation. + * Will be stripped of any trailing separators for a consistent internal representation. * @return A new [Directory] created from the components. */ fun from(volume: StorageVolume, relativePath: String) = @@ -97,8 +103,9 @@ class Directory private constructor(val volume: StorageVolume, val relativePath: * Create a new directory from a document tree URI. This is a huge violation of the document * tree URI contract, but it's also the only one can sensibly work with these uris in the * UI, and it doesn't exactly matter since we never write or read directory. + * * @param storageManager [StorageManager] in order to obtain the [StorageVolume] specified - * in the given URI. + * in the given URI. * @param uri The URI string to parse into a [Directory]. * @return A new [Directory] parsed from the URI, or null if the URI is not valid. */ @@ -123,26 +130,29 @@ class Directory private constructor(val volume: StorageVolume, val relativePath: /** * Represents the configuration for specific directories to filter to/from when loading music. + * * @param dirs A list of [Directory] instances. How these are interpreted depends on [shouldInclude] * @param shouldInclude True if the library should only load from the [Directory] instances, false - * if the library should not load from the [Directory] instances. + * if the library should not load from the [Directory] instances. * @author Alexander Capehart (OxygenCobalt) */ data class MusicDirectories(val dirs: List, val shouldInclude: Boolean) /** * A mime type of a file. Only intended for display. + * * @param fromExtension The mime type obtained by analyzing the file extension. * @param fromFormat The mime type obtained by analyzing the file format. Null if could not be - * obtained. + * obtained. * @author Alexander Capehart (OxygenCobalt) */ data class MimeType(val fromExtension: String, val fromFormat: String?) { /** * Resolve the mime type into a human-readable format name, such as "Ogg Vorbis". + * * @param context [Context] required to obtain human-readable strings. * @return A human-readable name for this mime type. Will first try [fromFormat], then falling - * back to [fromExtension], and then null if that fails. + * back to [fromExtension], and then null if that fails. */ fun resolveName(context: Context): String? { // We try our best to produce a more readable name for the common audio formats. diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/MediaStoreExtractor.kt index 32912061d..1669ae516 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/MediaStoreExtractor.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * MediaStoreExtractor.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 @@ -42,22 +43,25 @@ import org.oxycblt.auxio.util.logD * music extraction process and primarily intended for redundancy for files not natively supported * by other extractors. Solely relying on this is not recommended, as it often produces bad * metadata. + * * @author Alexander Capehart (OxygenCobalt) */ interface MediaStoreExtractor { /** * Query the media database. + * * @return A new [Query] returned from the media database. */ suspend fun query(): Query /** * Consume the [Cursor] loaded after [query]. + * * @param query The [Query] to consume. * @param cache A [Cache] used to avoid extracting metadata for cached songs, or null if no - * [Cache] was available. + * [Cache] was available. * @param incompleteSongs A channel where songs that could not be retrieved from the [Cache] - * should be sent to. + * should be sent to. * @param completeSongs A channel where completed songs should be sent to. */ suspend fun consume( @@ -79,6 +83,7 @@ interface MediaStoreExtractor { companion object { /** * Create a framework-backed instance. + * * @param context [Context] required. * @param musicSettings [MusicSettings] required. * @return A new [MediaStoreExtractor] that will work best on the device's API level. @@ -158,27 +163,28 @@ private abstract class BaseMediaStoreExtractor( context.contentResolverSafe.useQuery( MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, arrayOf(MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME)) { genreCursor -> - val idIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID) - val nameIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME) + val idIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID) + val nameIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME) - while (genreCursor.moveToNext()) { - val id = genreCursor.getLong(idIndex) - val name = genreCursor.getStringOrNull(nameIndex) ?: continue + while (genreCursor.moveToNext()) { + val id = genreCursor.getLong(idIndex) + val name = genreCursor.getStringOrNull(nameIndex) ?: continue - context.contentResolverSafe.useQuery( - MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, id), - arrayOf(MediaStore.Audio.Genres.Members._ID)) { cursor -> - val songIdIndex = - cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID) + context.contentResolverSafe.useQuery( + MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, id), + arrayOf(MediaStore.Audio.Genres.Members._ID)) { cursor -> + val songIdIndex = + cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID) - while (cursor.moveToNext()) { - // Assume that a song can't inhabit multiple genre entries, as I doubt - // MediaStore is actually aware that songs can have multiple genres. - genreNamesMap[cursor.getLong(songIdIndex)] = name - } + while (cursor.moveToNext()) { + // Assume that a song can't inhabit multiple genre entries, as I + // doubt + // MediaStore is actually aware that songs can have multiple genres. + genreNamesMap[cursor.getLong(songIdIndex)] = name + } + } } } - } logD("Finished initialization in ${System.currentTimeMillis() - start}ms") return wrapQuery(cursor, genreNamesMap) @@ -232,15 +238,17 @@ private abstract class BaseMediaStoreExtractor( /** * The companion template to add to the projection's selector whenever arguments are added by * [addDirToSelector]. + * * @see addDirToSelector */ protected abstract val dirSelectorTemplate: String /** * Add a [Directory] to the given list of projection selector arguments. + * * @param dir The [Directory] to add. * @param args The destination list to append selector arguments to that are analogous to the - * given [Directory]. + * given [Directory]. * @return true if the [Directory] was added, false otherwise. * @see dirSelectorTemplate */ @@ -431,6 +439,7 @@ private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSet /** * A [BaseMediaStoreExtractor] that implements common behavior supported from API 29 onwards. + * * @param context [Context] required to query the media database. * @author Alexander Capehart (OxygenCobalt) */ @@ -494,8 +503,8 @@ private abstract class BaseApi29MediaStoreExtractor( /** * A [BaseMediaStoreExtractor] that completes the music loading process in a way compatible with at - * API - * 29. + * API 29. + * * @param context [Context] required to query the media database. * @author Alexander Capehart (OxygenCobalt) */ @@ -535,6 +544,7 @@ private class Api29MediaStoreExtractor(context: Context, musicSettings: MusicSet /** * A [BaseMediaStoreExtractor] that completes the music loading process in a way compatible from API * 30 onwards. + * * @param context [Context] required to query the media database. * @author Alexander Capehart (OxygenCobalt) */ @@ -584,8 +594,9 @@ private class Api30MediaStoreExtractor(context: Context, musicSettings: MusicSet * Unpack the track number from a combined track + disc [Int] field. These fields appear within * MediaStore's TRACK column, and combine the track and disc value into a single field where the * disc number is the 4th+ digit. + * * @return The track number extracted from the combined integer value, or null if the value was - * zero. + * zero. */ private fun Int.unpackTrackNo() = transformPositionField(mod(1000), null) @@ -593,6 +604,7 @@ private fun Int.unpackTrackNo() = transformPositionField(mod(1000), null) * Unpack the disc number from a combined track + disc [Int] field. These fields appear within * MediaStore's TRACK column, and combine the track and disc value into a single field where the * disc number is the 4th+ digit. + * * @return The disc number extracted from the combined integer field, or null if the value was zero. */ private fun Int.unpackDiscNo() = transformPositionField(div(1000), null) diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt index f6ea77464..fdd59f0d1 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * MusicDirsDialog.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 @@ -41,6 +42,7 @@ import org.oxycblt.auxio.util.showToast /** * Dialog that manages the music dirs setting. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint @@ -149,8 +151,9 @@ class MusicDirsDialog : /** * Add a Document Tree [Uri] chosen by the user to the current [MusicDirectories] instance. + * * @param uri The document tree [Uri] to add, chosen by the user. Will do nothing if the [Uri] - * is null or not valid. + * is null or not valid. */ private fun addDocumentTreeUriToDirs(uri: Uri?) { if (uri == null) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/StorageModule.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/StorageModule.kt index 6869788d1..11d0e5650 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/StorageModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/StorageModule.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * StorageModule.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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt index 6de6c4b3f..0ed74674e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * StorageUtil.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 @@ -42,10 +43,11 @@ val Context.contentResolverSafe: ContentResolver /** * A shortcut for querying the [ContentResolver] database. + * * @param uri The [Uri] of content to retrieve. * @param projection A list of SQL columns to query from the database. * @param selector A SQL selection statement to filter results. Spaces where arguments should be - * filled in are represented with a "?". + * filled in are represented with a "?". * @param args The arguments used for the selector. * @return A [Cursor] of the queried values, organized by the column projection. * @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor]. @@ -61,13 +63,14 @@ fun ContentResolver.safeQuery( /** * A shortcut for [safeQuery] with [use] applied, automatically cleaning up the [Cursor]'s resources * when no longer used. + * * @param uri The [Uri] of content to retrieve. * @param projection A list of SQL columns to query from the database. * @param selector A SQL selection statement to filter results. Spaces where arguments should be - * filled in are represented with a "?". + * filled in are represented with a "?". * @param args The arguments used for the selector. * @param block The block of code to run with the queried [Cursor]. Will not be ran if the [Cursor] - * is empty. + * is empty. * @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor]. * @see ContentResolver.query */ @@ -84,6 +87,7 @@ private val EXTERNAL_COVERS_URI = Uri.parse("content://media/external/audio/albu /** * Convert a [MediaStore] Song ID into a [Uri] to it's audio file. + * * @return An external storage audio file [Uri]. May not exist. * @see ContentUris.withAppendedId * @see MediaStore.Audio.Media.EXTERNAL_CONTENT_URI @@ -94,6 +98,7 @@ fun Long.toAudioUri() = /** * Convert a [MediaStore] Album ID into a [Uri] to it's system-provided album cover. This cover will * be fast to load, but will be lower quality. + * * @return An external storage image [Uri]. May not exist. * @see ContentUris.withAppendedId */ @@ -105,6 +110,7 @@ fun Long.toCoverUri() = ContentUris.withAppendedId(EXTERNAL_COVERS_URI, this) /** * 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") @@ -114,6 +120,7 @@ private val SM_API21_GET_VOLUME_LIST_METHOD: Method by /** * 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. + * * @see StorageVolume.getDirectory */ @Suppress("NewApi") @@ -122,6 +129,7 @@ private val SV_API21_GET_PATH_METHOD: Method by lazyReflectedMethod(StorageVolum /** * The [StorageVolume] considered the "primary" volume by the system, obtained in a * version-compatible manner. + * * @see StorageManager.getPrimaryStorageVolume * @see StorageVolume.isPrimary */ @@ -131,6 +139,7 @@ val StorageManager.primaryStorageVolumeCompat: StorageVolume /** * The list of [StorageVolume]s currently recognized by [StorageManager], in a version-compatible * manner. + * * @see StorageManager.getStorageVolumes */ val StorageManager.storageVolumesCompat: List @@ -145,6 +154,7 @@ val StorageManager.storageVolumesCompat: List /** * The the absolute path to this [StorageVolume]'s directory within the file-system, in a * version-compatible manner. Will be null if the [StorageVolume] cannot be read. + * * @see StorageVolume.getDirectory */ val StorageVolume.directoryCompat: String? @@ -164,6 +174,7 @@ val StorageVolume.directoryCompat: String? /** * Get the human-readable description of this volume, such as "Internal Shared Storage". + * * @param context [Context] required to obtain human-readable string resources. * @return A human-readable name for this volume. */ @@ -173,6 +184,7 @@ fun StorageVolume.getDescriptionCompat(context: Context): String = getDescriptio /** * If this [StorageVolume] is considered the "Primary" volume where the Android System is kept. May * still be a removable volume. + * * @see StorageVolume.isPrimary */ val StorageVolume.isPrimaryCompat: Boolean @@ -181,6 +193,7 @@ val StorageVolume.isPrimaryCompat: Boolean /** * If this storage is "emulated", i.e intrinsic to the device, obtained in a version compatible * manner. + * * @see StorageVolume.isEmulated */ val StorageVolume.isEmulatedCompat: Boolean @@ -198,6 +211,7 @@ val StorageVolume.isInternalCompat: Boolean /** * The unique identifier for this [StorageVolume], obtained in a version compatible manner. Can be * null. + * * @see StorageVolume.getUuid */ val StorageVolume.uuidCompat: String? @@ -206,6 +220,7 @@ val StorageVolume.uuidCompat: String? /** * The current state of this [StorageVolume], such as "mounted" or "read-only", obtained in a * version compatible manner. + * * @see StorageVolume.getState */ val StorageVolume.stateCompat: String @@ -214,6 +229,7 @@ val StorageVolume.stateCompat: String /** * Returns the name of this volume that can be used to interact with [MediaStore], in a version * compatible manner. Will be null if the volume is not scanned by [MediaStore]. + * * @see StorageVolume.getMediaStoreVolumeName */ val StorageVolume.mediaStoreVolumeNameCompat: String? diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt index 5820a096d..321724d2c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * Indexer.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 @@ -68,6 +69,7 @@ interface Indexer { * Register a [Controller] for this instance. This instance will handle any commands to start * the music loading process. There can be only one [Controller] at a time. Will invoke all * [Listener] methods to initialize the instance with the current state. + * * @param controller The [Controller] to register. Will do nothing if already registered. */ fun registerController(controller: Controller) @@ -75,8 +77,9 @@ interface Indexer { /** * Unregister the [Controller] from this instance, prevent it from recieving any further * commands. + * * @param controller The [Controller] to unregister. Must be the current [Controller]. Does - * nothing if invoked by another [Controller] implementation. + * nothing if invoked by another [Controller] implementation. */ fun unregisterController(controller: Controller) @@ -84,14 +87,16 @@ interface Indexer { * Register the [Listener] for this instance. This can be used to receive rapid-fire updates to * the current music loading state. There can be only one [Listener] at a time. Will invoke all * [Listener] methods to initialize the instance with the current state. + * * @param listener The [Listener] to add. */ fun registerListener(listener: Listener) /** * Unregister a [Listener] from this instance, preventing it from recieving any further updates. + * * @param listener The [Listener] to unregister. Must be the current [Listener]. Does nothing if - * invoked by another [Listener] implementation. + * invoked by another [Listener] implementation. * @see Listener */ fun unregisterListener(listener: Listener) @@ -99,9 +104,10 @@ interface Indexer { /** * Start the indexing process. This should be done from in the background from [Controller]'s * context after a command has been received to start the process. + * * @param context [Context] required to load music. * @param withCache Whether to use the cache or not when loading. If false, the cache will still - * be written, but no cache entries will be loaded into the new library. + * be written, but no cache entries will be loaded into the new library. * @param scope The [CoroutineScope] to run the indexing job in. * @return The [Job] stacking the indexing status. */ @@ -111,8 +117,9 @@ interface Indexer { * Request that the music library should be reloaded. This should be used by components that do * not manage the indexing process in order to signal that the [Indexer.Controller] should call * [index] eventually. + * * @param withCache Whether to use the cache when loading music. Does nothing if there is no - * [Indexer.Controller]. + * [Indexer.Controller]. */ fun requestReindex(withCache: Boolean) @@ -126,6 +133,7 @@ interface Indexer { sealed class State { /** * Music loading is ongoing. + * * @param indexing The current music loading progress.. * @see Indexer.Indexing */ @@ -133,6 +141,7 @@ interface Indexer { /** * Music loading has completed. + * * @param result The outcome of the music loading process. */ data class Complete(val result: Result) : State() @@ -140,6 +149,7 @@ interface Indexer { /** * Represents the current progress of the music loader. Usually encapsulated in a [State]. + * * @see State.Indexing */ sealed class Indexing { @@ -150,6 +160,7 @@ interface Indexer { /** * Music loading has a definite progress. + * * @param current The current amount of songs that have been loaded. * @param total The projected total amount of songs that will be loaded. */ @@ -182,7 +193,7 @@ interface Indexer { * Notes: * - Null means that no loading is going on, but no load has completed either. * - [State.Complete] may represent a previous load, if the current loading process was - * canceled for one reason or another. + * canceled for one reason or another. */ fun onIndexerStateChanged(state: State?) } @@ -195,8 +206,9 @@ interface Indexer { /** * Called when a new music loading process was requested. Implementations should forward * this to [index]. + * * @param withCache Whether to use the cache or not when loading. If false, the cache should - * still be written, but no cache entries will be loaded into the new library. + * still be written, but no cache entries will be loaded into the new library. * @see index */ fun onStartIndexing(withCache: Boolean) @@ -390,8 +402,9 @@ constructor( * Emit a new [Indexer.State.Indexing] state. This can be used to signal the current state of * the music loading process to external code. Assumes that the callee has already checked if * they have not been canceled and thus have the ability to emit a new state. + * * @param indexing The new [Indexer.Indexing] state to emit, or null if no loading process is - * occurring. + * occurring. */ @Synchronized private fun emitIndexing(indexing: Indexer.Indexing?) { @@ -409,8 +422,9 @@ constructor( * Emit a new [Indexer.State.Complete] state. This can be used to signal the completion of the * music loading process to external code. Will check if the callee has not been canceled and * thus has the ability to emit a new state + * * @param result The new [Result] to emit, representing the outcome of the music loading - * process. + * process. */ private suspend fun emitCompletion(result: Result) { yield() 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 67d301c36..5f72fac18 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * IndexerNotifications.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 @@ -29,6 +30,7 @@ import org.oxycblt.auxio.util.newMainPendingIntent /** * A dynamic [ForegroundServiceNotification] that shows the current music loading state. + * * @param context [Context] required to create the notification. * @author Alexander Capehart (OxygenCobalt) */ @@ -53,6 +55,7 @@ class IndexingNotification(private val context: Context) : /** * Update this notification with the new music loading state. + * * @param indexing The new music loading state to display in the notification. * @return true if the notification updated, false otherwise */ @@ -90,6 +93,7 @@ class IndexingNotification(private val context: Context) : /** * A static [ForegroundServiceNotification] that signals to the user that the app is currently * monitoring the music library for changes. + * * @author Alexander Capehart (OxygenCobalt) */ class ObservingNotification(context: Context) : diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt index 81e22ce51..def283ba5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * IndexerService.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 @@ -50,9 +51,9 @@ import org.oxycblt.auxio.util.logD * This [Service] also handles automatic rescanning, as that is a similarly long-running background * operation that would be unsuitable elsewhere in the app. * - * TODO: Unify with PlaybackService as part of the service independence project - * * @author Alexander Capehart (OxygenCobalt) + * + * TODO: Unify with PlaybackService as part of the service independence project */ @AndroidEntryPoint class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener { @@ -176,6 +177,7 @@ class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener { /** * Update the current state to "Active", in which the service signals that music loading is * on-going. + * * @param state The current music loading state. */ private fun updateActiveSession(state: Indexer.Indexing) { diff --git a/app/src/main/java/org/oxycblt/auxio/picker/ArtistChoiceAdapter.kt b/app/src/main/java/org/oxycblt/auxio/picker/ArtistChoiceAdapter.kt index 48cc378ed..5c9ad275b 100644 --- a/app/src/main/java/org/oxycblt/auxio/picker/ArtistChoiceAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/picker/ArtistChoiceAdapter.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * ArtistChoiceAdapter.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 @@ -29,6 +30,7 @@ import org.oxycblt.auxio.util.inflater /** * An [RecyclerView.Adapter] that displays a list of [Artist] choices. + * * @param listener A [ClickableListListener] to bind interactions to. * @author OxygenCobalt. */ @@ -46,6 +48,7 @@ class ArtistChoiceAdapter(private val listener: ClickableListListener) : /** * Immediately update the [Artist] choices. + * * @param newArtists The new [Artist]s to show. */ fun submitList(newArtists: List) { @@ -64,6 +67,7 @@ class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) : DialogRecyclerView.ViewHolder(binding.root) { /** * Bind new data to this instance. + * * @param artist The new [Artist] to bind. * @param listener A [ClickableListListener] to bind interactions to. */ @@ -76,6 +80,7 @@ class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) : companion object { /** * Create a new instance. + * * @param parent The parent to inflate this instance from. * @return A new instance. */ diff --git a/app/src/main/java/org/oxycblt/auxio/picker/ArtistNavigationPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/picker/ArtistNavigationPickerDialog.kt index e998e9976..a43fc61a0 100644 --- a/app/src/main/java/org/oxycblt/auxio/picker/ArtistNavigationPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/picker/ArtistNavigationPickerDialog.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * ArtistNavigationPickerDialog.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 @@ -28,6 +29,7 @@ import org.oxycblt.auxio.ui.NavigationViewModel /** * An [ArtistPickerDialog] intended for when [Artist] navigation is ambiguous. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint diff --git a/app/src/main/java/org/oxycblt/auxio/picker/ArtistPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/picker/ArtistPickerDialog.kt index a4a57eda2..bf8e48265 100644 --- a/app/src/main/java/org/oxycblt/auxio/picker/ArtistPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/picker/ArtistPickerDialog.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * ArtistPickerDialog.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 @@ -35,6 +36,7 @@ import org.oxycblt.auxio.util.collectImmediately * The base class for dialogs that implements common behavior across all [Artist] pickers. These are * shown whenever what to do with an item's [Artist] is ambiguous, as there are multiple [Artist]'s * to choose from. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint diff --git a/app/src/main/java/org/oxycblt/auxio/picker/ArtistPlaybackPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/picker/ArtistPlaybackPickerDialog.kt index ea1916fda..36a73aa81 100644 --- a/app/src/main/java/org/oxycblt/auxio/picker/ArtistPlaybackPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/picker/ArtistPlaybackPickerDialog.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * ArtistPlaybackPickerDialog.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 @@ -31,6 +32,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull /** * An [ArtistPickerDialog] intended for when [Artist] playback is ambiguous. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint diff --git a/app/src/main/java/org/oxycblt/auxio/picker/GenreChoiceAdapter.kt b/app/src/main/java/org/oxycblt/auxio/picker/GenreChoiceAdapter.kt index 2cea397b5..b01b42928 100644 --- a/app/src/main/java/org/oxycblt/auxio/picker/GenreChoiceAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/picker/GenreChoiceAdapter.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * GenreChoiceAdapter.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 @@ -29,6 +30,7 @@ import org.oxycblt.auxio.util.inflater /** * An [RecyclerView.Adapter] that displays a list of [Genre] choices. + * * @param listener A [ClickableListListener] to bind interactions to. * @author OxygenCobalt. */ @@ -46,6 +48,7 @@ class GenreChoiceAdapter(private val listener: ClickableListListener) : /** * Immediately update the [Genre] choices. + * * @param newGenres The new [Genre]s to show. */ fun submitList(newGenres: List) { @@ -64,6 +67,7 @@ class GenreChoiceViewHolder(private val binding: ItemPickerChoiceBinding) : DialogRecyclerView.ViewHolder(binding.root) { /** * Bind new data to this instance. + * * @param genre The new [Genre] to bind. * @param listener A [ClickableListListener] to bind interactions to. */ @@ -76,6 +80,7 @@ class GenreChoiceViewHolder(private val binding: ItemPickerChoiceBinding) : companion object { /** * Create a new instance. + * * @param parent The parent to inflate this instance from. * @return A new instance. */ diff --git a/app/src/main/java/org/oxycblt/auxio/picker/GenrePlaybackPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/picker/GenrePlaybackPickerDialog.kt index 81e5607b7..17441f9a9 100644 --- a/app/src/main/java/org/oxycblt/auxio/picker/GenrePlaybackPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/picker/GenrePlaybackPickerDialog.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * GenrePlaybackPickerDialog.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 @@ -39,6 +40,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull /** * A picker [ViewBindingDialogFragment] intended for when [Genre] playback is ambiguous. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint diff --git a/app/src/main/java/org/oxycblt/auxio/picker/PickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/picker/PickerViewModel.kt index 800f069ad..c10892dd5 100644 --- a/app/src/main/java/org/oxycblt/auxio/picker/PickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/picker/PickerViewModel.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * PickerViewModel.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 @@ -29,6 +30,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull /** * a [ViewModel] that manages the current music picker state. Make it so that the dialogs just * contain the music themselves and then exit if the library changes. + * * @author Alexander Capehart (OxygenCobalt) */ @HiltViewModel @@ -62,6 +64,7 @@ class PickerViewModel @Inject constructor(private val musicRepository: MusicRepo /** * Set a new [currentItem] from it's [Music.UID]. + * * @param uid The [Music.UID] of the [Song] to update to. */ fun setItemUid(uid: Music.UID) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/ActionMode.kt b/app/src/main/java/org/oxycblt/auxio/playback/ActionMode.kt index f1130b5dc..d1bd00da5 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/ActionMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/ActionMode.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * ActionMode.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 @@ -22,6 +23,7 @@ import org.oxycblt.auxio.IntegerTable /** * Represents a configuration option for what kind of "secondary" action to show in a particular UI * context. + * * @author Alexander Capehart (OxygenCobalt) */ enum class ActionMode { @@ -34,6 +36,7 @@ enum class ActionMode { /** * The integer representation of this instance. + * * @see fromIntCode */ val intCode: Int @@ -47,6 +50,7 @@ enum class ActionMode { companion object { /** * Convert a [ActionMode] integer representation into an instance. + * * @param intCode An integer representation of a [ActionMode] * @return The corresponding [ActionMode], or null if the [ActionMode] is invalid. * @see ActionMode.intCode diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt index 5ecf71d21..bda248be7 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * PlaybackBarFragment.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 @@ -35,6 +36,7 @@ import org.oxycblt.auxio.util.getColorCompat /** * A [ViewBindingFragment] that shows the current playback state in a compact manner. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt index fcb07e937..a1a7b86b9 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBottomSheetBehavior.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * PlaybackBottomSheetBehavior.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 @@ -31,6 +32,7 @@ import org.oxycblt.auxio.util.getDimen /** * The [BaseBottomSheetBehavior] for the playback bottom sheet. This bottom sheet + * * @author Alexander Capehart (OxygenCobalt) */ class PlaybackBottomSheetBehavior(context: Context, attributeSet: AttributeSet?) : 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 dc05fc8c1..55233002d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackModule.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * PlaybackModule.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 @@ -17,9 +18,25 @@ package org.oxycblt.auxio.playback +import android.content.Context +import com.google.android.exoplayer2.extractor.ExtractorsFactory +import com.google.android.exoplayer2.extractor.flac.FlacExtractor +import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor +import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor +import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor +import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor +import com.google.android.exoplayer2.extractor.ogg.OggExtractor +import com.google.android.exoplayer2.extractor.ts.AdtsExtractor +import com.google.android.exoplayer2.extractor.wav.WavExtractor +import com.google.android.exoplayer2.source.MediaSource +import com.google.android.exoplayer2.source.ProgressiveMediaSource +import com.google.android.exoplayer2.upstream.ContentDataSource +import com.google.android.exoplayer2.upstream.DataSource import dagger.Binds import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton import org.oxycblt.auxio.playback.state.PlaybackStateManager @@ -33,3 +50,35 @@ interface PlaybackModule { fun stateManager(playbackManager: PlaybackStateManagerImpl): PlaybackStateManager @Binds fun settings(playbackSettings: PlaybackSettingsImpl): PlaybackSettings } + +@Module +@InstallIn(SingletonComponent::class) +class ExoPlayerModule { + @Provides + fun mediaSourceFactory( + dataSourceFactory: DataSource.Factory, + extractorsFactory: ExtractorsFactory + ): MediaSource.Factory = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory) + + @Provides + fun dataSourceFactory(@ApplicationContext context: Context) = + // We only ever open conte tURIs, so only provide those data sources. + DataSource.Factory { ContentDataSource(context) } + + @Provides + fun extractorsFactory() = ExtractorsFactory { + // Define our own extractors so we can exclude non-audio parsers. + // Ordering is derived from the DefaultExtractorsFactory's optimized ordering: + // https://docs.google.com/document/d/1w2mKaWMxfz2Ei8-LdxqbPs1VLe_oudB-eryXXw9OvQQ. + arrayOf( + FlacExtractor(), + WavExtractor(), + FragmentedMp4Extractor(), + Mp4Extractor(), + OggExtractor(), + MatroskaExtractor(), + // Enable constant bitrate seeking so that certain MP3s/AACs are seekable + AdtsExtractor(AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING), + Mp3Extractor(Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING)) + } +} 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 49f018f85..67137eeb9 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * PlaybackPanelFragment.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 @@ -47,6 +48,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat /** * A [ViewBindingFragment] more information about the currently playing song, alongside all * available controls. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint 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 218c83b89..2a65c85a3 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * PlaybackSettings.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 @@ -31,6 +32,7 @@ import org.oxycblt.auxio.util.logD /** * User configuration specific to the playback system. + * * @author Alexander Capehart (OxygenCobalt) */ interface PlaybackSettings : Settings { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackUtil.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackUtil.kt index 5872e2f41..17b4fcc12 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackUtil.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * PlaybackUtil.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,52 +22,60 @@ import android.text.format.DateUtils /** * Convert milliseconds into deci-seconds (1/10th of a second). + * * @return A converted deci-second value. */ fun Long.msToDs() = floorDiv(100) /** * Convert milliseconds into seconds. + * * @return A converted second value. */ fun Long.msToSecs() = floorDiv(1000) /** * Convert deci-seconds (1/10th of a second) into milliseconds. + * * @return A converted millisecond value. */ fun Long.dsToMs() = times(100) /** * Convert deci-seconds (1/10th of a second) into seconds. + * * @return A converted second value. */ fun Long.dsToSecs() = floorDiv(10) /** * Convert seconds into milliseconds. + * * @return A converted millisecond value. */ fun Long.secsToMs() = times(1000) /** * Convert a millisecond value into a string duration. + * * @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:-- - * will be returned if the second value is 0. + * will be returned if the second value is 0. */ fun Long.formatDurationMs(isElapsed: Boolean) = msToSecs().formatDurationSecs(isElapsed) /** * // * Format a deci-second value (1/10th of a second) into a string duration. + * * @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:-- - * will be returned if the second value is 0. + * will be returned if the second value is 0. */ fun Long.formatDurationDs(isElapsed: Boolean) = dsToSecs().formatDurationSecs(isElapsed) /** * Convert a second value into a string duration. + * * @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:-- - * will be returned if the second value is 0. + * will be returned if the second value is 0. */ fun Long.formatDurationSecs(isElapsed: Boolean): String { if (!isElapsed && this == 0L) { 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 82b6a8fdd..9301bb341 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * PlaybackViewModel.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 @@ -30,9 +31,12 @@ import org.oxycblt.auxio.music.* import org.oxycblt.auxio.playback.persist.PersistenceRepository import org.oxycblt.auxio.playback.queue.Queue import org.oxycblt.auxio.playback.state.* +import org.oxycblt.auxio.util.Event +import org.oxycblt.auxio.util.MutableEvent /** * An [ViewModel] that provides a safe UI frontend for the current playback state. + * * @author Alexander Capehart (OxygenCobalt) */ @HiltViewModel @@ -72,21 +76,22 @@ constructor( val isShuffled: StateFlow get() = _isShuffled - private val _artistPlaybackPickerSong = MutableStateFlow(null) + private val _artistPlaybackPickerSong = MutableEvent() /** * Flag signaling to open a picker dialog in order to resolve an ambiguous choice when playing a * [Song] from one of it's [Artist]s. + * * @see playFromArtist */ - val artistPickerSong: StateFlow + val artistPickerSong: Event get() = _artistPlaybackPickerSong - private val _genrePlaybackPickerSong = MutableStateFlow(null) + private val _genrePlaybackPickerSong = MutableEvent() /** * Flag signaling to open a picker dialog in order to resolve an ambiguous choice when playing a * [Song] from one of it's [Genre]s. */ - val genrePickerSong: StateFlow + val genrePickerSong: Event get() = _genrePlaybackPickerSong /** The current action to show on the playback bar. */ @@ -112,9 +117,9 @@ constructor( _song.value = queue.currentSong } - override fun onQueueChanged(queue: Queue, change: Queue.ChangeResult) { + override fun onQueueChanged(queue: Queue, change: Queue.Change) { // Other types of queue changes preserve the current song. - if (change == Queue.ChangeResult.SONG) { + if (change.type == Queue.Change.Type.SONG) { _song.value = queue.currentSong } } @@ -163,6 +168,7 @@ constructor( * - 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. + * * @param song The [Song] to play. * @param playbackMode The [MusicMode] to play from. */ @@ -177,9 +183,10 @@ constructor( /** * Play a [Song] from one of it's [Artist]s. + * * @param song The [Song] to play. * @param artist The [Artist] to play from. Must be linked to the [Song]. If null, the user will - * be prompted on what artist to play. Defaults to null. + * be prompted on what artist to play. Defaults to null. */ fun playFromArtist(song: Song, artist: Artist? = null) { if (artist != null) { @@ -187,24 +194,16 @@ constructor( } else if (song.artists.size == 1) { playImpl(song, song.artists[0]) } else { - _artistPlaybackPickerSong.value = song + _artistPlaybackPickerSong.put(song) } } - /** - * Mark the [Artist] playback choice process as complete. This should occur when the [Artist] - * choice dialog is opened after this flag is detected. - * @see playFromArtist - */ - fun finishPlaybackArtistPicker() { - _artistPlaybackPickerSong.value = null - } - /** * PLay a [Song] from one of it's [Genre]s. + * * @param song The [Song] to play. * @param genre The [Genre] to play from. Must be linked to the [Song]. If null, the user will - * be prompted on what artist to play. Defaults to null. + * be prompted on what artist to play. Defaults to null. */ fun playFromGenre(song: Song, genre: Genre? = null) { if (genre != null) { @@ -212,39 +211,34 @@ constructor( } else if (song.genres.size == 1) { playImpl(song, song.genres[0]) } else { - _genrePlaybackPickerSong.value = song + _genrePlaybackPickerSong.put(song) } } - /** - * Mark the [Genre] playback choice process as complete. This should occur when the [Genre] - * choice dialog is opened after this flag is detected. - * @see playFromGenre - */ - fun finishPlaybackGenrePicker() { - _genrePlaybackPickerSong.value = null - } - /** * Play an [Album]. + * * @param album The [Album] to play. */ fun play(album: Album) = playImpl(null, album, false) /** * Play an [Artist]. + * * @param artist The [Artist] to play. */ fun play(artist: Artist) = playImpl(null, artist, false) /** * Play a [Genre]. + * * @param genre The [Genre] to play. */ fun play(genre: Genre) = playImpl(null, genre, false) /** * Play a [Music] selection. + * * @param selection The selection to play. */ fun play(selection: List) = @@ -252,24 +246,28 @@ constructor( /** * Shuffle an [Album]. + * * @param album The [Album] to shuffle. */ fun shuffle(album: Album) = playImpl(null, album, true) /** * Shuffle an [Artist]. + * * @param artist The [Artist] to shuffle. */ fun shuffle(artist: Artist) = playImpl(null, artist, true) /** * Shuffle an [Genre]. + * * @param genre The [Genre] to shuffle. */ fun shuffle(genre: Genre) = playImpl(null, genre, true) /** * Shuffle a [Music] selection. + * * @param selection The selection to shuffle. */ fun shuffle(selection: List) = @@ -298,6 +296,7 @@ constructor( /** * Start the given [InternalPlayer.Action] to be completed eventually. This can be used to * enqueue a playback action at startup to then occur when the music library is fully loaded. + * * @param action The [InternalPlayer.Action] to perform eventually. */ fun startAction(action: InternalPlayer.Action) { @@ -308,6 +307,7 @@ constructor( /** * Seek to the given position in the currently playing [Song]. + * * @param positionDs The position to seek to, in deci-seconds (1/10th of a second). */ fun seekTo(positionDs: Long) { @@ -328,6 +328,7 @@ constructor( /** * Add a [Song] to the top of the queue. + * * @param song The [Song] to add. */ fun playNext(song: Song) { @@ -336,6 +337,7 @@ constructor( /** * Add a [Album] to the top of the queue. + * * @param album The [Album] to add. */ fun playNext(album: Album) { @@ -344,6 +346,7 @@ constructor( /** * Add a [Artist] to the top of the queue. + * * @param artist The [Artist] to add. */ fun playNext(artist: Artist) { @@ -352,6 +355,7 @@ constructor( /** * Add a [Genre] to the top of the queue. + * * @param genre The [Genre] to add. */ fun playNext(genre: Genre) { @@ -360,6 +364,7 @@ constructor( /** * Add a selection to the top of the queue. + * * @param selection The [Music] selection to add. */ fun playNext(selection: List) { @@ -368,6 +373,7 @@ constructor( /** * Add a [Song] to the end of the queue. + * * @param song The [Song] to add. */ fun addToQueue(song: Song) { @@ -376,6 +382,7 @@ constructor( /** * Add a [Album] to the end of the queue. + * * @param album The [Album] to add. */ fun addToQueue(album: Album) { @@ -384,6 +391,7 @@ constructor( /** * Add a [Artist] to the end of the queue. + * * @param artist The [Artist] to add. */ fun addToQueue(artist: Artist) { @@ -392,6 +400,7 @@ constructor( /** * Add a [Genre] to the end of the queue. + * * @param genre The [Genre] to add. */ fun addToQueue(genre: Genre) { @@ -400,6 +409,7 @@ constructor( /** * Add a selection to the end of the queue. + * * @param selection The [Music] selection to add. */ fun addToQueue(selection: List) { @@ -420,6 +430,7 @@ constructor( /** * Toggle [repeatMode] (ex. from [RepeatMode.NONE] to [RepeatMode.TRACK]) + * * @see RepeatMode.increment */ fun toggleRepeatMode() { @@ -430,6 +441,7 @@ constructor( /** * Force-save the current playback state. + * * @param onDone Called when the save is completed with true if successful, and false otherwise. */ fun savePlaybackState(onDone: (Boolean) -> Unit) { @@ -440,6 +452,7 @@ constructor( /** * Clear the current playback state. + * * @param onDone Called when the wipe is completed with true if successful, and false otherwise. */ fun wipePlaybackState(onDone: (Boolean) -> Unit) { @@ -448,8 +461,9 @@ constructor( /** * Force-restore the current playback state. + * * @param onDone Called when the restoration is completed with true if successful, and false - * otherwise. + * otherwise. */ fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) { viewModelScope.launch { @@ -468,9 +482,10 @@ constructor( /** * Convert the given selection to a list of [Song]s. + * * @param selection The selection of [Music] to convert. * @return A [Song] list containing the child items of any [MusicParent] instances in the list - * alongside the unchanged [Song]s or the original selection. + * alongside the unchanged [Song]s or the original selection. */ private fun selectionToSongs(selection: List): List { return selection.flatMap { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt index be41d0413..a61731213 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * PersistenceDatabase.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 @@ -32,6 +33,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode /** * Provides raw access to the database storing the persisted playback state. + * * @author Alexander Capehart */ @Database( @@ -42,12 +44,14 @@ import org.oxycblt.auxio.playback.state.RepeatMode abstract class PersistenceDatabase : RoomDatabase() { /** * Get the current [PlaybackStateDao]. + * * @return A [PlaybackStateDao] providing control of the database's playback state tables. */ abstract fun playbackStateDao(): PlaybackStateDao /** * Get the current [QueueDao]. + * * @return A [QueueDao] providing control of the database's queue tables. */ abstract fun queueDao(): QueueDao @@ -63,12 +67,14 @@ abstract class PersistenceDatabase : RoomDatabase() { /** * Provides control of the persisted playback state table. + * * @author Alexander Capehart (OxygenCobalt) */ @Dao interface PlaybackStateDao { /** * Get the previously persisted [PlaybackState]. + * * @return The previously persisted [PlaybackState], or null if one was not present. */ @Query("SELECT * FROM ${PlaybackState.TABLE_NAME} WHERE id = 0") @@ -79,6 +85,7 @@ interface PlaybackStateDao { /** * Insert a new [PlaybackState] into the database. + * * @param state The [PlaybackState] to insert. */ @Insert(onConflict = OnConflictStrategy.ABORT) suspend fun insertState(state: PlaybackState) @@ -86,18 +93,21 @@ interface PlaybackStateDao { /** * Provides control of the persisted queue state tables. + * * @author Alexander Capehart (OxygenCobalt) */ @Dao interface QueueDao { /** * Get the previously persisted queue heap. + * * @return A list of persisted [QueueHeapItem]s wrapping each heap item. */ @Query("SELECT * FROM ${QueueHeapItem.TABLE_NAME}") suspend fun getHeap(): List /** * Get the previously persisted queue mapping. + * * @return A list of persisted [QueueMappingItem]s wrapping each heap item. */ @Query("SELECT * FROM ${QueueMappingItem.TABLE_NAME}") @@ -111,12 +121,14 @@ interface QueueDao { /** * Insert new heap entries into the database. + * * @param heap The list of wrapped [QueueHeapItem]s to insert. */ @Insert(onConflict = OnConflictStrategy.ABORT) suspend fun insertHeap(heap: List) /** * Insert new mapping entries into the database. + * * @param mapping The list of wrapped [QueueMappingItem] to insert. */ @Insert(onConflict = OnConflictStrategy.ABORT) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceModule.kt b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceModule.kt index b43fb17b0..76af5369b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceModule.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * PersistenceModule.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 diff --git a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt index 854906036..9ce5d89d2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * PersistenceRepository.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 @@ -27,17 +28,20 @@ import org.oxycblt.auxio.util.logE /** * Manages the persisted playback state in a structured manner. + * * @author Alexander Capehart (OxygenCobalt) */ interface PersistenceRepository { /** * Read the previously persisted [PlaybackStateManager.SavedState]. + * * @param library The [Library] required to de-serialize the [PlaybackStateManager.SavedState]. */ suspend fun readState(library: Library): PlaybackStateManager.SavedState? /** * Persist a new [PlaybackStateManager.SavedState]. + * * @param state The [PlaybackStateManager.SavedState] to persist. */ suspend fun saveState(state: PlaybackStateManager.SavedState?): Boolean 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 9a2b44eb9..1ccf3b4ab 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * Queue.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 @@ -19,6 +20,7 @@ package org.oxycblt.auxio.playback.queue import kotlin.random.Random import kotlin.random.nextInt +import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Song @@ -45,31 +47,39 @@ interface Queue { val isShuffled: Boolean /** * Resolve this queue into a more conventional list of [Song]s. + * * @return A list of [Song] corresponding to the current queue mapping. */ fun resolve(): List /** - * Represents the possible changes that can occur during certain queue mutation events. The - * precise meanings of these differ somewhat depending on the type of mutation done. + * Represents the possible changes that can occur during certain queue mutation events. + * + * @param type The [Type] of the change to the internal queue state. + * @param instructions The update done to the resolved queue list. */ - enum class ChangeResult { - /** Only the mapping has changed. */ - MAPPING, - /** The mapping has changed, and the index also changed to align with it. */ - INDEX, - /** - * The current song has changed, possibly alongside the mapping and index depending on the - * context. - */ - SONG + data class Change(val type: Type, val instructions: UpdateInstructions) { + enum class Type { + /** Only the mapping has changed. */ + MAPPING, + + /** The mapping has changed, and the index also changed to align with it. */ + INDEX, + + /** + * The current song has changed, possibly alongside the mapping and index depending on + * the context. + */ + SONG + } } /** * An immutable representation of the queue state. + * * @param heap The heap of [Song]s that are/were used in the queue. This can be modified with - * null values to represent [Song]s that were "lost" from the heap without having to change - * other values. + * null values to represent [Song]s that were "lost" from the heap without having to change + * other values. * @param orderedMapping The mapping of the [heap] to an ordered queue. * @param shuffledMapping The mapping of the [heap] to a shuffled queue. * @param index The index of the currently playing [Song] at the time of serialization. @@ -85,9 +95,10 @@ interface Queue { /** * Remaps the [heap] of this instance based on the given mapping function and copies it into * a new [SavedState]. + * * @param transform Code to remap the existing [Song] heap into a new [Song] heap. This - * **MUST** be the same size as the original heap. [Song] instances that could not be - * converted should be replaced with null in the new heap. + * **MUST** be the same size as the original heap. [Song] instances that could not be + * converted should be replaced with null in the new heap. * @throws IllegalStateException If the invariant specified by [transform] is violated. */ inline fun remap(transform: (Song?) -> Song?) = @@ -121,6 +132,7 @@ class EditableQueue : Queue { /** * Go to a particular index in the queue. + * * @param to The index of the [Song] to start playing, in the current queue mapping. * @return true if the queue jumped to that position, false otherwise. */ @@ -134,11 +146,12 @@ class EditableQueue : Queue { /** * Start a new queue configuration. + * * @param play The [Song] to play, or null to start from a random position. * @param queue The queue of [Song]s to play. Must contain [play]. This list will become the - * heap internally. + * heap internally. * @param shuffled Whether to shuffle the queue or not. This changes the interpretation of - * [queue]. + * [queue]. */ fun start(play: Song?, queue: List, shuffled: Boolean) { heap = queue.toMutableList() @@ -152,6 +165,7 @@ class EditableQueue : Queue { /** * Re-order the queue. + * * @param shuffled Whether the queue should be shuffled or not. */ fun reorder(shuffled: Boolean) { @@ -185,18 +199,11 @@ class EditableQueue : Queue { /** * Add [Song]s to the top of the queue. Will start playback if nothing is playing. + * * @param songs The [Song]s to add. - * @return [Queue.ChangeResult.MAPPING] if added to an existing queue, or - * [Queue.ChangeResult.SONG] if there was no prior playback and these enqueued [Song]s start new - * playback. + * @return A [Queue.Change] instance that reflects the changes made. */ - fun playNext(songs: List): Queue.ChangeResult { - if (orderedMapping.isEmpty()) { - // No playback, start playing these songs. - start(songs[0], songs, false) - return Queue.ChangeResult.SONG - } - + fun playNext(songs: List): Queue.Change { val heapIndices = songs.map(::addSongToHeap) if (shuffledMapping.isNotEmpty()) { // Add the new songs in front of the current index in the shuffled mapping and in front @@ -209,23 +216,17 @@ class EditableQueue : Queue { orderedMapping.addAll(index + 1, heapIndices) } check() - return Queue.ChangeResult.MAPPING + return Queue.Change( + Queue.Change.Type.MAPPING, UpdateInstructions.Add(index + 1, songs.size)) } /** * Add [Song]s to the end of the queue. Will start playback if nothing is playing. + * * @param songs The [Song]s to add. - * @return [Queue.ChangeResult.MAPPING] if added to an existing queue, or - * [Queue.ChangeResult.SONG] if there was no prior playback and these enqueued [Song]s start new - * playback. + * @return A [Queue.Change] instance that reflects the changes made. */ - fun addToQueue(songs: List): Queue.ChangeResult { - if (orderedMapping.isEmpty()) { - // No playback, start playing these songs. - start(songs[0], songs, false) - return Queue.ChangeResult.SONG - } - + fun addToQueue(songs: List): Queue.Change { val heapIndices = songs.map(::addSongToHeap) // Can simple append the new songs to the end of both mappings. orderedMapping.addAll(heapIndices) @@ -233,18 +234,18 @@ class EditableQueue : Queue { shuffledMapping.addAll(heapIndices) } check() - return Queue.ChangeResult.MAPPING + return Queue.Change( + Queue.Change.Type.MAPPING, UpdateInstructions.Add(index + 1, songs.size)) } /** * Move a [Song] at the given position to a new position. + * * @param src The position of the [Song] to move. * @param dst The destination position of the [Song]. - * @return [Queue.ChangeResult.MAPPING] if the move occurred after the current index, - * [Queue.ChangeResult.INDEX] if the move occurred before or at the current index, requiring it - * to be mutated. + * @return A [Queue.Change] instance that reflects the changes made. */ - fun move(src: Int, dst: Int): Queue.ChangeResult { + fun move(src: Int, dst: Int): Queue.Change { if (shuffledMapping.isNotEmpty()) { // Move songs only in the shuffled mapping. There is no sane analogous form of // this for the ordered mapping. @@ -264,21 +265,20 @@ class EditableQueue : Queue { else -> { // Nothing to do. check() - return Queue.ChangeResult.MAPPING + return Queue.Change(Queue.Change.Type.MAPPING, UpdateInstructions.Move(src, dst)) } } check() - return Queue.ChangeResult.INDEX + return Queue.Change(Queue.Change.Type.INDEX, UpdateInstructions.Move(src, dst)) } /** * Remove a [Song] at the given position. + * * @param at The position of the [Song] to remove. - * @return [Queue.ChangeResult.MAPPING] if the removed [Song] was after the current index, - * [Queue.ChangeResult.INDEX] if the removed [Song] was before the current index, and - * [Queue.ChangeResult.SONG] if the currently playing [Song] was removed. + * @return A [Queue.Change] instance that reflects the changes made. */ - fun remove(at: Int): Queue.ChangeResult { + fun remove(at: Int): Queue.Change { if (shuffledMapping.isNotEmpty()) { // Remove the specified index in the shuffled mapping and the analogous song in the // ordered mapping. @@ -293,24 +293,25 @@ class EditableQueue : Queue { // of the player to be completely invalidated. It's generally easier to not remove the // song and retain player state consistency. - val result = + val type = when { // We just removed the currently playing song. - index == at -> Queue.ChangeResult.SONG + index == at -> Queue.Change.Type.SONG // Index was ahead of removed song, shift back to preserve consistency. index > at -> { index -= 1 - Queue.ChangeResult.INDEX + Queue.Change.Type.INDEX } // Nothing to do - else -> Queue.ChangeResult.MAPPING + else -> Queue.Change.Type.MAPPING } check() - return result + return Queue.Change(type, UpdateInstructions.Remove(at)) } /** * Convert the current state of this instance into a [Queue.SavedState]. + * * @return A new [Queue.SavedState] reflecting the exact state of the queue when called. */ fun toSavedState() = @@ -321,6 +322,7 @@ class EditableQueue : Queue { /** * Update this instance from the given [Queue.SavedState]. + * * @param savedState A [Queue.SavedState] with a valid queue representation. */ fun applySavedState(savedState: Queue.SavedState) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt index 2b67c5b3e..2230fe7a2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * QueueAdapter.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 @@ -27,10 +28,7 @@ import com.google.android.material.shape.MaterialShapeDrawable import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemQueueSongBinding import org.oxycblt.auxio.list.EditableListListener -import org.oxycblt.auxio.list.adapter.BasicListInstructions -import org.oxycblt.auxio.list.adapter.DiffAdapter -import org.oxycblt.auxio.list.adapter.ListDiffer -import org.oxycblt.auxio.list.adapter.PlayingIndicatorAdapter +import org.oxycblt.auxio.list.adapter.* import org.oxycblt.auxio.list.recycler.SongViewHolder import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.resolveNames @@ -38,12 +36,12 @@ import org.oxycblt.auxio.util.* /** * A [RecyclerView.Adapter] that shows an editable list of queue items. + * * @param listener A [EditableListListener] to bind interactions to. * @author Alexander Capehart (OxygenCobalt) */ class QueueAdapter(private val listener: EditableListListener) : - DiffAdapter( - ListDiffer.Blocking(QueueSongViewHolder.DIFF_CALLBACK)) { + FlexibleListAdapter(QueueSongViewHolder.DIFF_CALLBACK) { // Since PlayingIndicator adapter relies on an item value, we cannot use it for this // adapter, as one item can appear at several points in the UI. Use a similar implementation // with an index value instead. @@ -72,6 +70,7 @@ class QueueAdapter(private val listener: EditableListListener) : /** * Set the position of the currently playing item in the queue. This will mark the item as * playing and any previous items as played. + * * @param index The position of the currently playing item in the queue. * @param isPlaying Whether playback is ongoing or paused. */ @@ -99,6 +98,7 @@ class QueueAdapter(private val listener: EditableListListener) : /** * A [PlayingIndicatorAdapter.ViewHolder] that displays a queue [Song]. Use [from] to create an * instance. + * * @author Alexander Capehart (OxygenCobalt) */ class QueueSongViewHolder private constructor(private val binding: ItemQueueSongBinding) : @@ -142,6 +142,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong /** * Bind new data to this instance. + * * @param song The new [Song] to bind. * @param listener A [EditableListListener] to bind interactions to. */ @@ -164,13 +165,13 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong companion object { /** * Create a new instance. + * * @param parent The parent to inflate this instance from. * @return A new instance. */ fun from(parent: View) = QueueSongViewHolder(ItemQueueSongBinding.inflate(parent.context.inflater)) - // TODO: This is not good enough, I need to compare item indices as well. /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = SongViewHolder.DIFF_CALLBACK } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt index 9796cdc34..d2043f373 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueBottomSheetBehavior.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * QueueBottomSheetBehavior.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 @@ -34,6 +35,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat /** * The [BaseBottomSheetBehavior] for the queue bottom sheet. This is placed within the playback * sheet and automatically arranges itself to show the playback bar at the top. + * * @author Alexander Capehart (OxygenCobalt) */ class QueueBottomSheetBehavior(context: Context, attributeSet: AttributeSet?) : diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt index c8308ef70..5b61eb7c4 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * QueueDragCallback.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 @@ -31,8 +32,6 @@ import org.oxycblt.auxio.util.logD * A highly customized [ItemTouchHelper.Callback] that enables some extra eye candy in the queue UI, * such as an animation when lifting items. * - * TODO: Why is item movement so expensive??? - * * @author Alexander Capehart (OxygenCobalt) */ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHelper.Callback() { @@ -127,9 +126,11 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder - ) = - playbackModel.moveQueueDataItems( + ): Boolean { + logD("${viewHolder.bindingAdapterPosition} ${target.bindingAdapterPosition}") + return playbackModel.moveQueueDataItems( viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) + } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { playbackModel.removeQueueDataItem(viewHolder.bindingAdapterPosition) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index 0e71bb55f..e39348451 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * QueueFragment.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 @@ -28,7 +29,6 @@ import dagger.hilt.android.AndroidEntryPoint import kotlin.math.min import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.list.EditableListListener -import org.oxycblt.auxio.list.adapter.BasicListInstructions import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.ViewBindingFragment @@ -36,6 +36,7 @@ import org.oxycblt.auxio.util.collectImmediately /** * A [ViewBindingFragment] that displays an editable queue. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint @@ -80,6 +81,9 @@ class QueueFragment : ViewBindingFragment(), EditableListL super.onDestroyBinding(binding) touchHelper = null binding.queueRecycler.adapter = null + // Avoid possible race conditions that could cause a bad instruction to be consumed + // during list initialization and crash the app. Could happen if the user is fast enough. + queueModel.queueInstructions.consume() } override fun onClick(item: Song, viewHolder: RecyclerView.ViewHolder) { @@ -101,13 +105,12 @@ class QueueFragment : ViewBindingFragment(), EditableListL val binding = requireBinding() // Replace or diff the queue depending on the type of change it is. - val instructions = queueModel.queueListInstructions - queueAdapter.submitList(queue, instructions?.update ?: BasicListInstructions.DIFF) + queueAdapter.update(queue, queueModel.queueInstructions.consume()) // Update position in list (and thus past/future items) queueAdapter.setPosition(index, isPlaying) // If requested, scroll to a new item (occurs when the index moves) - val scrollTo = instructions?.scrollTo + val scrollTo = queueModel.scrollTo.consume() if (scrollTo != null) { val lmm = binding.queueRecycler.layoutManager as LinearLayoutManager val start = lmm.findFirstCompletelyVisibleItemPosition() @@ -128,7 +131,5 @@ class QueueFragment : ViewBindingFragment(), EditableListL min(queue.lastIndex, scrollTo + (end - start))) } } - - queueModel.finishInstructions() } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt index 1e09a1bbf..13099ef7b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * QueueViewModel.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 @@ -22,10 +23,12 @@ 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.adapter.BasicListInstructions +import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackStateManager +import org.oxycblt.auxio.util.Event +import org.oxycblt.auxio.util.MutableEvent /** * A [ViewModel] that manages the current queue state and allows navigation through the queue. @@ -39,29 +42,33 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt private val _queue = MutableStateFlow(listOf()) /** The current queue. */ val queue: StateFlow> = _queue + private val _queueInstructions = MutableEvent() + /** Instructions for how to update [queue] in the UI. */ + val queueInstructions: Event = _queueInstructions + private val _scrollTo = MutableEvent() + /** Controls whether the queue should be force-scrolled to a particular location. */ + val scrollTo: Event + get() = _scrollTo private val _index = MutableStateFlow(playbackManager.queue.index) /** The index of the currently playing song in the queue. */ val index: StateFlow get() = _index - /** Specifies how to update the list when the queue changes. */ - var queueListInstructions: ListInstructions? = null - init { playbackManager.addListener(this) } override fun onIndexMoved(queue: Queue) { - queueListInstructions = ListInstructions(null, queue.index) + _scrollTo.put(queue.index) _index.value = queue.index } - override fun onQueueChanged(queue: Queue, change: Queue.ChangeResult) { + override fun onQueueChanged(queue: Queue, change: Queue.Change) { // Queue changed trivially due to item mo -> Diff queue, stay at current index. - queueListInstructions = ListInstructions(BasicListInstructions.DIFF, null) + _queueInstructions.put(change.instructions) _queue.value = queue.resolve() - if (change != Queue.ChangeResult.MAPPING) { + if (change.type != Queue.Change.Type.MAPPING) { // Index changed, make sure it remains updated without actually scrolling to it. _index.value = queue.index } @@ -69,14 +76,16 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt override fun onQueueReordered(queue: Queue) { // Queue changed completely -> Replace queue, update index - queueListInstructions = ListInstructions(BasicListInstructions.REPLACE, queue.index) + _queueInstructions.put(UpdateInstructions.Replace(0)) + _scrollTo.put(queue.index) _queue.value = queue.resolve() _index.value = queue.index } override fun onNewPlayback(queue: Queue, parent: MusicParent?) { // Entirely new queue -> Replace queue, update index - queueListInstructions = ListInstructions(BasicListInstructions.REPLACE, queue.index) + _queueInstructions.put(UpdateInstructions.Replace(0)) + _scrollTo.put(queue.index) _queue.value = queue.resolve() _index.value = queue.index } @@ -88,8 +97,9 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt /** * Start playing the the queue item at the given index. + * * @param adapterIndex The index of the queue item to play. Does nothing if the index is out of - * range. + * range. */ fun goto(adapterIndex: Int) { playbackManager.goto(adapterIndex) @@ -97,8 +107,9 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt /** * Remove a queue item at the given index. + * * @param adapterIndex The index of the queue item to play. Does nothing if the index is out of - * range. + * range. */ fun removeQueueDataItem(adapterIndex: Int) { if (adapterIndex !in queue.value.indices) { @@ -109,6 +120,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt /** * Move a queue item from one index to another index. + * * @param adapterFrom The index of the queue item to move. * @param adapterTo The destination index for the queue item. * @return true if the items were moved, false otherwise. @@ -120,11 +132,4 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt playbackManager.moveQueueItem(adapterFrom, adapterTo) return true } - - /** Signal that the specified [ListInstructions] in [queueListInstructions] were performed. */ - fun finishInstructions() { - queueListInstructions = null - } - - class ListInstructions(val update: BasicListInstructions?, val scrollTo: Int?) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt index 81db4bad3..07815dde4 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * PreAmpCustomizeDialog.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 @@ -31,6 +32,7 @@ import org.oxycblt.auxio.ui.ViewBindingDialogFragment /** * aa [ViewBindingDialogFragment] that allows user configuration of the current [ReplayGainPreAmp]. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt index c10dae5a3..0b1855a50 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGain.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * ReplayGain.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,6 +22,7 @@ import org.oxycblt.auxio.IntegerTable /** * The current ReplayGain configuration. + * * @author Alexander Capehart (OxygenCobalt) */ enum class ReplayGainMode { @@ -34,6 +36,7 @@ enum class ReplayGainMode { companion object { /** * Convert a [ReplayGainMode] integer representation into an instance. + * * @param intCode An integer representation of a [ReplayGainMode] * @return The corresponding [ReplayGainMode], or null if the [ReplayGainMode] is invalid. */ @@ -49,6 +52,7 @@ enum class ReplayGainMode { /** * The current ReplayGain pre-amp configuration. + * * @param with The pre-amp (in dB) to use when ReplayGain tags are present. * @param without The pre-amp (in dB) to use when ReplayGain tags are not present. * @author Alexander Capehart (OxygenCobalt) 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 29f19fde4..e9015c8d5 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * ReplayGainAudioProcessor.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 @@ -23,7 +24,6 @@ import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Tracks import com.google.android.exoplayer2.audio.AudioProcessor import com.google.android.exoplayer2.audio.BaseAudioProcessor -import com.google.android.exoplayer2.util.MimeTypes import java.nio.ByteBuffer import javax.inject.Inject import kotlin.math.pow @@ -60,8 +60,9 @@ constructor( /** * Add this instance to the components required for it to function correctly. + * * @param player The [Player] to attach to. Should already have this instance as an audio - * processor. + * processor. */ fun addToListeners(player: Player) { player.addListener(this) @@ -70,8 +71,9 @@ constructor( /** * Remove this instance from the components required for it to function correctly. + * * @param player The [Player] to detach from. Should already have this instance as an audio - * processor. + * processor. */ fun releaseFromListeners(player: Player) { player.removeListener(this) @@ -107,6 +109,7 @@ constructor( /** * Updates the volume adjustment based on the given [Format]. + * * @param format The [Format] of the currently playing track, or null if nothing is playing. */ private fun applyReplayGain(format: Format?) { @@ -158,6 +161,7 @@ constructor( /** * Parse ReplayGain information from the given [Format]. + * * @param format The [Format] to parse. * @return A [Adjustment] adjustment, or null if there were no valid adjustments. */ @@ -168,32 +172,28 @@ constructor( // Most ReplayGain tags are formatted as a simple decibel adjustment in a custom // replaygain_*_gain tag. - if (format.sampleMimeType != MimeTypes.AUDIO_OPUS) { - textTags.id3v2["TXXX:$TAG_RG_TRACK_GAIN"] - ?.run { first().parseReplayGainAdjustment() } - ?.let { trackGain = it } - textTags.id3v2["TXXX:$TAG_RG_ALBUM_GAIN"] - ?.run { first().parseReplayGainAdjustment() } - ?.let { albumGain = it } - textTags.vorbis[TAG_RG_ALBUM_GAIN] - ?.run { first().parseReplayGainAdjustment() } - ?.let { trackGain = it } - textTags.vorbis[TAG_RG_TRACK_GAIN] - ?.run { first().parseReplayGainAdjustment() } - ?.let { albumGain = it } - } else { - // Opus has it's own "r128_*_gain" ReplayGain specification, which requires dividing the - // adjustment by 256 to get the gain. This is used alongside the base adjustment - // intrinsic to the format to create the normalized adjustment. That base adjustment - // is already handled by the media framework, so we just need to apply the more - // specific adjustments. - textTags.vorbis[TAG_R128_TRACK_GAIN] - ?.run { first().parseReplayGainAdjustment() } - ?.let { trackGain = it / 256f } - textTags.vorbis[TAG_R128_ALBUM_GAIN] - ?.run { first().parseReplayGainAdjustment() } - ?.let { albumGain = it / 256f } - } + textTags.id3v2["TXXX:$TAG_RG_TRACK_GAIN"] + ?.run { first().parseReplayGainAdjustment() } + ?.let { trackGain = it } + textTags.id3v2["TXXX:$TAG_RG_ALBUM_GAIN"] + ?.run { first().parseReplayGainAdjustment() } + ?.let { albumGain = it } + textTags.vorbis[TAG_RG_ALBUM_GAIN] + ?.run { first().parseReplayGainAdjustment() } + ?.let { trackGain = it } + textTags.vorbis[TAG_RG_TRACK_GAIN] + ?.run { first().parseReplayGainAdjustment() } + ?.let { albumGain = it } + // Opus has it's own "r128_*_gain" ReplayGain specification, which requires dividing the + // adjustment by 256 to get the gain. This is used alongside the base adjustment + // intrinsic to the format to create the normalized adjustment. This is normally the only + // tag used for opus files, but some software still writes replay gain tags anyway. + textTags.vorbis[TAG_R128_TRACK_GAIN] + ?.run { first().parseReplayGainAdjustment() } + ?.let { trackGain = it / 256f } + textTags.vorbis[TAG_R128_ALBUM_GAIN] + ?.run { first().parseReplayGainAdjustment() } + ?.let { albumGain = it / 256f } return if (trackGain != 0f || albumGain != 0f) { Adjustment(trackGain, albumGain) @@ -204,6 +204,7 @@ constructor( /** * Parse a ReplayGain adjustment into a float value. + * * @return A parsed adjustment float, or null if the adjustment had invalid formatting. */ private fun String.parseReplayGainAdjustment() = @@ -259,6 +260,7 @@ constructor( /** * Always read a little-endian [Short] from the [ByteBuffer] at the given index. + * * @param at The index to read the [Short] from. */ private fun ByteBuffer.getLeShort(at: Int) = @@ -266,6 +268,7 @@ constructor( /** * Always write a little-endian [Short] at the end of the [ByteBuffer]. + * * @param short The [Short] to write. */ private fun ByteBuffer.putLeShort(short: Short) { @@ -275,6 +278,7 @@ constructor( /** * The resolved ReplayGain adjustment for a file. + * * @param track The track adjustment (in dB), or 0 if it is not present. * @param album The album adjustment (in dB), or 0 if it is not present. */ 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 7c2487458..3edbf8633 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * InternalPlayer.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 @@ -25,6 +26,7 @@ import org.oxycblt.auxio.music.Song /** * An interface for internal audio playback. This can be used to coordinate what occurs in the * background playback task. + * * @author Alexander Capehart (OxygenCobalt) */ interface InternalPlayer { @@ -36,6 +38,7 @@ interface InternalPlayer { /** * Load a new [Song] into the internal player. + * * @param song The [Song] to load, or null if playback should stop entirely. * @param play Whether to start playing when the [Song] is loaded. */ @@ -43,6 +46,7 @@ interface InternalPlayer { /** * Called when an [Action] has been queued and this [InternalPlayer] is available to handle it. + * * @param action The [Action] to perform. * @return true if the action was handled, false otherwise. */ @@ -50,19 +54,22 @@ interface InternalPlayer { /** * Get a [State] corresponding to the current player state. + * * @param durationMs The duration of the currently playing track, in milliseconds. Required - * since the internal player cannot obtain an accurate duration itself. + * since the internal player cannot obtain an accurate duration itself. */ fun getState(durationMs: Long): State /** * Seek to a given position in the internal player. + * * @param positionMs The position to seek to, in milliseconds. */ fun seekTo(positionMs: Long) /** * Set whether the player should play or not. + * * @param isPlaying Whether to play or pause the current playback. */ fun setPlaying(isPlaying: Boolean) @@ -80,6 +87,7 @@ interface InternalPlayer { /** * Start playing an audio file at the given [Uri]. + * * @param uri The [Uri] of the audio file to start playing. */ data class Open(val uri: Uri) : Action() @@ -101,8 +109,9 @@ interface InternalPlayer { ) { /** * Calculate the "real" playback position this instance contains, in milliseconds. + * * @return If paused, the original position will be returned. Otherwise, it will be the - * original position plus the time elapsed since this state was created. + * original position plus the time elapsed since this state was created. */ fun calculateElapsedPositionMs() = if (isAdvancing) { @@ -115,6 +124,7 @@ interface InternalPlayer { /** * Load this instance into a [PlaybackStateCompat]. + * * @param builder The [PlaybackStateCompat.Builder] to mutate. * @return The same [PlaybackStateCompat.Builder] for easy chaining. */ @@ -155,8 +165,9 @@ interface InternalPlayer { companion object { /** * Create a new instance. + * * @param isPlaying Whether the player is actively playing audio or set to play audio in - * the future. + * the future. * @param isAdvancing Whether the player is actively playing audio in this moment. * @param positionMs The current position of the player. */ 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 f00182b53..c15982e03 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * PlaybackStateManager.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 @@ -34,9 +35,9 @@ import org.oxycblt.auxio.util.logW * * This should ***NOT*** be used outside of the playback module. * - If you want to use the playback state in the UI, use PlaybackViewModel as it can withstand - * volatile UIs. + * volatile UIs. * - If you want to use the playback state with the ExoPlayer instance or system-side things, use - * PlaybackService. + * PlaybackService. * * Internal consumers should usually use [Listener], however the component that manages the player * itself should instead use [InternalPlayer]. @@ -58,6 +59,7 @@ interface PlaybackStateManager { /** * Add a [Listener] to this instance. This can be used to receive changes in the playback state. * Will immediately invoke [Listener] methods to initialize the instance with the current state. + * * @param listener The [Listener] to add. * @see Listener */ @@ -65,8 +67,9 @@ interface PlaybackStateManager { /** * Remove a [Listener] from this instance, preventing it from receiving any further updates. + * * @param listener The [Listener] to remove. Does nothing if the [Listener] was never added in - * the first place. + * the first place. * @see Listener */ fun removeListener(listener: Listener) @@ -75,25 +78,28 @@ interface PlaybackStateManager { * Register an [InternalPlayer] for this instance. This instance will handle translating the * current playback state into audio playback. There can be only one [InternalPlayer] at a time. * Will invoke [InternalPlayer] methods to initialize the instance with the current state. + * * @param internalPlayer The [InternalPlayer] to register. Will do nothing if already - * registered. + * registered. */ fun registerInternalPlayer(internalPlayer: InternalPlayer) /** * Unregister the [InternalPlayer] from this instance, prevent it from receiving any further * commands. + * * @param internalPlayer The [InternalPlayer] to unregister. Must be the current - * [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation. + * [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation. */ fun unregisterInternalPlayer(internalPlayer: InternalPlayer) /** * Start new playback. + * * @param song A particular [Song] to play, or null to play the first [Song] in the new queue. * @param queue The queue of [Song]s to play from. * @param parent The [MusicParent] to play from, or null if to play from an non-specific - * collection of "All [Song]s". + * collection of "All [Song]s". * @param shuffled Whether to shuffle or not. */ fun play(song: Song?, parent: MusicParent?, queue: List, shuffled: Boolean) @@ -112,36 +118,42 @@ interface PlaybackStateManager { /** * Play a [Song] at the given position in the queue. + * * @param index The position of the [Song] in the queue to start playing. */ fun goto(index: Int) /** * Add [Song]s to the top of the queue. + * * @param songs The [Song]s to add. */ fun playNext(songs: List) /** * Add a [Song] to the top of the queue. + * * @param song The [Song] to add. */ fun playNext(song: Song) = playNext(listOf(song)) /** * Add [Song]s to the end of the queue. + * * @param songs The [Song]s to add. */ fun addToQueue(songs: List) /** * Add a [Song] to the end of the queue. + * * @param song The [Song] to add. */ fun addToQueue(song: Song) = addToQueue(listOf(song)) /** * Move a [Song] in the queue. + * * @param src The position of the [Song] to move in the queue. * @param dst The destination position in the queue. */ @@ -149,25 +161,29 @@ interface PlaybackStateManager { /** * Remove a [Song] from the queue. + * * @param at The position of the [Song] to remove in the queue. */ fun removeQueueItem(at: Int) /** * (Re)shuffle or (Re)order this instance. + * * @param shuffled Whether to shuffle the queue or not. */ fun reorder(shuffled: Boolean) /** * Synchronize the state of this instance with the current [InternalPlayer]. + * * @param internalPlayer The [InternalPlayer] to synchronize with. Must be the current - * [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation. + * [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation. */ fun synchronizeState(internalPlayer: InternalPlayer) /** * Start a [InternalPlayer.Action] for the current [InternalPlayer] to handle eventually. + * * @param action The [InternalPlayer.Action] to perform. */ fun startAction(action: InternalPlayer.Action) @@ -175,19 +191,22 @@ interface PlaybackStateManager { /** * Request that the pending [InternalPlayer.Action] (if any) be passed to the given * [InternalPlayer]. + * * @param internalPlayer The [InternalPlayer] to synchronize with. Must be the current - * [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation. + * [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation. */ fun requestAction(internalPlayer: InternalPlayer) /** * Update whether playback is ongoing or not. + * * @param isPlaying Whether playback is ongoing or not. */ fun setPlaying(isPlaying: Boolean) /** * Seek to the given position in the currently playing [Song]. + * * @param positionMs The position to seek to, in milliseconds. */ fun seekTo(positionMs: Long) @@ -197,16 +216,18 @@ interface PlaybackStateManager { /** * Converts the current state of this instance into a [SavedState]. + * * @return An immutable [SavedState] that is analogous to the current state, or null if nothing - * is currently playing. + * is currently playing. */ fun toSavedState(): SavedState? /** * Restores this instance from the given [SavedState]. + * * @param savedState The [SavedState] to restore from. * @param destructive Whether to disregard the prior playback state and overwrite it with this - * [SavedState]. + * [SavedState]. */ fun applySavedState(savedState: SavedState, destructive: Boolean) @@ -218,26 +239,30 @@ interface PlaybackStateManager { /** * Called when the position of the currently playing item has changed, changing the current * [Song], but no other queue attribute has changed. + * * @param queue The new [Queue]. */ fun onIndexMoved(queue: Queue) {} /** - * Called when the [Queue] changed in a manner outlined by the given [Queue.ChangeResult]. + * Called when the [Queue] changed in a manner outlined by the given [Queue.Change]. + * * @param queue The new [Queue]. - * @param change The type of [Queue.ChangeResult] that occurred. + * @param change The type of [Queue.Change] that occurred. */ - fun onQueueChanged(queue: Queue, change: Queue.ChangeResult) {} + fun onQueueChanged(queue: Queue, change: Queue.Change) {} /** * Called when the [Queue] has changed in a non-trivial manner (such as re-shuffling), but * the currently playing [Song] has not. + * * @param queue The new [Queue]. */ fun onQueueReordered(queue: Queue) {} /** * Called when a new playback configuration was created. + * * @param queue The new [Queue]. * @param parent The new [MusicParent] being played from, or null if playing from all songs. */ @@ -245,12 +270,14 @@ interface PlaybackStateManager { /** * Called when the state of the [InternalPlayer] changes. + * * @param state The new state of the [InternalPlayer]. */ fun onStateChanged(state: InternalPlayer.State) {} /** * Called when the [RepeatMode] changes. + * * @param repeatMode The new [RepeatMode]. */ fun onRepeatChanged(repeatMode: RepeatMode) {} @@ -258,6 +285,7 @@ interface PlaybackStateManager { /** * A condensed representation of the playback state that can be persisted. + * * @param parent The [MusicParent] item currently being played from. * @param queueState The [Queue.SavedState] * @param positionMs The current position in the currently played song, in ms @@ -396,31 +424,19 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { @Synchronized override fun playNext(songs: List) { - val internalPlayer = internalPlayer ?: return - when (queue.playNext(songs)) { - Queue.ChangeResult.MAPPING -> notifyQueueChanged(Queue.ChangeResult.MAPPING) - Queue.ChangeResult.SONG -> { - // Enqueueing actually started a new playback session from all songs. - parent = null - internalPlayer.loadSong(queue.currentSong, true) - notifyNewPlayback() - } - Queue.ChangeResult.INDEX -> error("Unreachable") + if (queue.currentSong == null) { + play(songs[0], null, songs, false) + } else { + notifyQueueChanged(queue.playNext(songs)) } } @Synchronized override fun addToQueue(songs: List) { - val internalPlayer = internalPlayer ?: return - when (queue.addToQueue(songs)) { - Queue.ChangeResult.MAPPING -> notifyQueueChanged(Queue.ChangeResult.MAPPING) - Queue.ChangeResult.SONG -> { - // Enqueueing actually started a new playback session from all songs. - parent = null - internalPlayer.loadSong(queue.currentSong, true) - notifyNewPlayback() - } - Queue.ChangeResult.INDEX -> error("Unreachable") + if (queue.currentSong == null) { + play(songs[0], null, songs, false) + } else { + notifyQueueChanged(queue.addToQueue(songs)) } } @@ -435,7 +451,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { val internalPlayer = internalPlayer ?: return logD("Removing item at $at") val change = queue.remove(at) - if (change == Queue.ChangeResult.SONG) { + if (change.type == Queue.Change.Type.SONG) { internalPlayer.loadSong(queue.currentSong, playerState.isPlaying) } notifyQueueChanged(change) @@ -541,7 +557,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { } } - private fun notifyQueueChanged(change: Queue.ChangeResult) { + private fun notifyQueueChanged(change: Queue.Change) { for (callback in listeners) { callback.onQueueChanged(queue, change) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/RepeatMode.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/RepeatMode.kt index e4da1aa72..ac0f34ee9 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/RepeatMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/RepeatMode.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * RepeatMode.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 @@ -22,6 +23,7 @@ import org.oxycblt.auxio.R /** * Represents the current repeat mode of the player. + * * @author Alexander Capehart (OxygenCobalt) */ enum class RepeatMode { @@ -44,6 +46,7 @@ enum class RepeatMode { /** * Increment the mode. + * * @return If [NONE], [ALL]. If [ALL], [TRACK]. If [TRACK], [NONE]. */ fun increment() = @@ -55,6 +58,7 @@ enum class RepeatMode { /** * The integer representation of this instance. + * * @see fromIntCode */ val icon: Int @@ -77,6 +81,7 @@ enum class RepeatMode { companion object { /** * Convert a [RepeatMode] integer representation into an instance. + * * @param intCode An integer representation of a [RepeatMode] * @return The corresponding [RepeatMode], or null if the [RepeatMode] is invalid. * @see RepeatMode.intCode diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/BluetoothHeadsetReceiver.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/BluetoothHeadsetReceiver.kt index 667d7c3ce..c8dbabc83 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/BluetoothHeadsetReceiver.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/BluetoothHeadsetReceiver.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * BluetoothHeadsetReceiver.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 @@ -24,6 +25,7 @@ import android.content.Intent /** * A [BroadcastReceiver] that starts music playback when a bluetooth headset is connected. + * * @author seijikun, OxygenCobalt */ class BluetoothHeadsetReceiver : BroadcastReceiver() { 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 02e4ff557..6c19d42b6 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * MediaButtonReceiver.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 @@ -28,6 +29,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager /** * A [BroadcastReceiver] that forwards [Intent.ACTION_MEDIA_BUTTON] [Intent]s to [PlaybackService]. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint 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 c1a099636..6194839f5 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * MediaSessionComponent.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 @@ -47,6 +48,7 @@ import org.oxycblt.auxio.util.logD /** * A component that mirrors the current playback state into the [MediaSessionCompat] and * [NotificationComponent]. + * * @author Alexander Capehart (OxygenCobalt) */ class MediaSessionComponent @@ -79,6 +81,7 @@ constructor( /** * Forward a system media button [Intent] to the [MediaSessionCompat]. + * * @param intent The [Intent.ACTION_MEDIA_BUTTON] [Intent] to forward. */ fun handleMediaButtonIntent(intent: Intent) { @@ -87,6 +90,7 @@ constructor( /** * Register a [Listener] for notification updates to this service. + * * @param listener The [Listener] to register. */ fun registerListener(listener: Listener) { @@ -115,16 +119,15 @@ constructor( invalidateSessionState() } - override fun onQueueChanged(queue: Queue, change: Queue.ChangeResult) { + override fun onQueueChanged(queue: Queue, change: Queue.Change) { updateQueue(queue) - when (change) { + when (change.type) { // Nothing special to do with mapping changes. - Queue.ChangeResult.MAPPING -> {} + Queue.Change.Type.MAPPING -> {} // Index changed, ensure playback state's index changes. - Queue.ChangeResult.INDEX -> invalidateSessionState() + Queue.Change.Type.INDEX -> invalidateSessionState() // Song changed, ensure metadata changes. - Queue.ChangeResult.SONG -> - updateMediaMetadata(queue.currentSong, playbackManager.parent) + Queue.Change.Type.SONG -> updateMediaMetadata(queue.currentSong, playbackManager.parent) } } @@ -271,10 +274,11 @@ constructor( /** * Upload a new [MediaMetadataCompat] based on the current playback state to the * [MediaSessionCompat] and [NotificationComponent]. + * * @param song The current [Song] to create the [MediaMetadataCompat] from, or null if no [Song] - * is currently playing. + * is currently playing. * @param parent The current [MusicParent] to create the [MediaMetadataCompat] from, or null if - * playback is currently occuring from all songs. + * playback is currently occuring from all songs. */ private fun updateMediaMetadata(song: Song?, parent: MusicParent?) { if (song == null) { @@ -338,6 +342,7 @@ constructor( /** * Upload a new queue to the [MediaSessionCompat]. + * * @param queue The current queue to upload. */ private fun updateQueue(queue: Queue) { @@ -367,8 +372,8 @@ constructor( logD("Updating media session playback state") val state = - // InternalPlayer.State handles position/state information. - playbackManager.playerState + // InternalPlayer.State handles position/state information. + playbackManager.playerState .intoPlaybackState(PlaybackStateCompat.Builder()) .setActions(ACTIONS) // Active queue ID corresponds to the indices we populated prior, use them here. @@ -426,6 +431,7 @@ constructor( interface Listener { /** * Called when the [NotificationComponent] changes, requiring it to be re-posed. + * * @param notification The new [NotificationComponent]. */ fun onPostNotification(notification: NotificationComponent) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt index 21cb16676..a6410a274 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * NotificationComponent.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 @@ -36,6 +37,7 @@ import org.oxycblt.auxio.util.newMainPendingIntent /** * The playback notification component. Due to race conditions regarding notification updates, this * component is not self-sufficient. [MediaSessionComponent] should be used instead of manage it. + * * @author Alexander Capehart (OxygenCobalt) */ @SuppressLint("RestrictedApi") @@ -67,6 +69,7 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes /** * Update the currently shown metadata in this notification. + * * @param metadata The [MediaMetadataCompat] to display in this notification. */ fun updateMetadata(metadata: MediaMetadataCompat) { @@ -86,6 +89,7 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes /** * Update the playing state shown in this notification. + * * @param isPlaying Whether playback should be indicated as ongoing or paused. */ fun updatePlaying(isPlaying: Boolean) { @@ -94,6 +98,7 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes /** * Update the secondary action in this notification to show the current [RepeatMode]. + * * @param repeatMode The current [RepeatMode]. */ fun updateRepeatMode(repeatMode: RepeatMode) { @@ -102,6 +107,7 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes /** * Update the secondary action in this notification to show the current shuffle state. + * * @param isShuffled Whether the queue is currently shuffled or not. */ fun updateShuffled(isShuffled: Boolean) { 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 aa4497534..4dd18fb85 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * PlaybackService.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 @@ -36,7 +37,7 @@ import com.google.android.exoplayer2.audio.AudioCapabilities import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer import com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer import com.google.android.exoplayer2.mediacodec.MediaCodecSelector -import com.google.android.exoplayer2.source.DefaultMediaSourceFactory +import com.google.android.exoplayer2.source.MediaSource import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -44,7 +45,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.oxycblt.auxio.BuildConfig -import org.oxycblt.auxio.music.AudioOnlyExtractors import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.Song @@ -71,11 +71,10 @@ import org.oxycblt.auxio.widgets.WidgetProvider * not the source of truth for the state, but rather the means to control system-side playback. Both * of those tasks are what [PlaybackStateManager] is for. * - * TODO: Refactor lifecycle to run completely headless (i.e no activity needed) - * - * TODO: Android Auto - * * @author Alexander Capehart (OxygenCobalt) + * + * TODO: Refactor lifecycle to run completely headless (i.e no activity needed) + * TODO: Android Auto */ @AndroidEntryPoint class PlaybackService : @@ -86,6 +85,7 @@ class PlaybackService : MusicRepository.Listener { // Player components private lateinit var player: ExoPlayer + @Inject lateinit var mediaSourceFactory: MediaSource.Factory @Inject lateinit var replayGainProcessor: ReplayGainAudioProcessor // System backend components @@ -115,9 +115,6 @@ class PlaybackService : override fun onCreate() { super.onCreate() - // Define our own extractors so we can exclude non-audio parsers. - // Ordering is derived from the DefaultExtractorsFactory's optimized ordering: - // https://docs.google.com/document/d/1w2mKaWMxfz2Ei8-LdxqbPs1VLe_oudB-eryXXw9OvQQ. // Since Auxio is a music player, only specify an audio renderer to save // battery/apk size/cache size val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ -> @@ -134,7 +131,7 @@ class PlaybackService : player = ExoPlayer.Builder(this, audioRenderer) - .setMediaSourceFactory(DefaultMediaSourceFactory(this, AudioOnlyExtractors)) + .setMediaSourceFactory(mediaSourceFactory) // Enable automatic WakeLock support .setWakeMode(C.WAKE_MODE_LOCAL) .setAudioAttributes( @@ -230,11 +227,7 @@ class PlaybackService : // No song, stop playback and foreground state. logD("Nothing playing, stopping playback") player.stop() - if (openAudioEffectSession) { - // Make sure to close the audio session when we stop playback. - broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION) - openAudioEffectSession = false - } + stopAndSave() return } @@ -242,15 +235,6 @@ class PlaybackService : logD("Loading ${song.rawName}") player.setMediaItem(MediaItem.fromUri(song.uri)) player.prepare() - - if (!openAudioEffectSession) { - // Android does not like it if you start an audio effect session without having - // something within your player buffer. Make sure we only start one when we load - // a song. - broadcastAudioEffectAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION) - openAudioEffectSession = true - } - player.playWhenReady = play } @@ -267,9 +251,21 @@ class PlaybackService : override fun onEvents(player: Player, events: Player.Events) { super.onEvents(player, events) - if (events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED) && player.playWhenReady) { - // Mark that we have started playing so that the notification can now be posted. - hasPlayed = true + if (events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) { + if (player.playWhenReady) { + // Mark that we have started playing so that the notification can now be posted. + hasPlayed = true + if (!openAudioEffectSession) { + // Convention to start an audioeffect session on play/pause rather than + // start/stop + broadcastAudioEffectAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION) + openAudioEffectSession = true + } + } else if (openAudioEffectSession) { + // Make sure to close the audio session when we stop playback. + broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION) + openAudioEffectSession = false + } } // Any change to the analogous isPlaying, isAdvancing, or positionMs values require diff --git a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt index a81abaec5..7023e9361 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/ui/AnimatedMaterialButton.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * AnimatedMaterialButton.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 @@ -28,6 +29,7 @@ import org.oxycblt.auxio.util.getInteger /** * A [MaterialButton] that automatically morphs from a circle to a squircle shape appearance when * [isActivated] changes. + * * @author Alexander Capehart (OxygenCobalt) */ class AnimatedMaterialButton diff --git a/app/src/main/java/org/oxycblt/auxio/playback/ui/ForcedLTRFrameLayout.kt b/app/src/main/java/org/oxycblt/auxio/playback/ui/ForcedLTRFrameLayout.kt index 37448989c..7d5a6da19 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/ui/ForcedLTRFrameLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/ui/ForcedLTRFrameLayout.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * ForcedLTRFrameLayout.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 @@ -26,6 +27,7 @@ import android.widget.FrameLayout * A [FrameLayout] that programmatically overrides the child layout to a left-to-right (LTR) layout * direction. This is useful for "Timeline" elements that Material Design recommends be LTR in all * cases. This layout can only contain one child, to prevent conflicts with other layout components. + * * @author Alexander Capehart (OxygenCobalt) */ open class ForcedLTRFrameLayout diff --git a/app/src/main/java/org/oxycblt/auxio/playback/ui/StyledSeekBar.kt b/app/src/main/java/org/oxycblt/auxio/playback/ui/StyledSeekBar.kt index 8df8662e4..e37cdf660 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/ui/StyledSeekBar.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/ui/StyledSeekBar.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * StyledSeekBar.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 @@ -29,6 +30,7 @@ import org.oxycblt.auxio.util.logD /** * A wrapper around [Slider] that shows position and duration values and sanitizes input to reduce * crashes from invalid values. + * * @author Alexander Capehart (OxygenCobalt) */ class StyledSeekBar @@ -112,6 +114,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 /** * Called when the internal [Slider] was scrubbed to a new position, requesting that a seek * be performed. + * * @param positionDs The position to seek to, in deci-seconds (1/10th of a second). */ fun onSeekConfirmed(positionDs: Long) diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt index 2ec81b783..6957cf2fa 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * SearchAdapter.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 @@ -20,8 +21,6 @@ package org.oxycblt.auxio.search import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.list.* -import org.oxycblt.auxio.list.adapter.BasicListInstructions -import org.oxycblt.auxio.list.adapter.ListDiffer import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.list.recycler.* @@ -30,12 +29,12 @@ import org.oxycblt.auxio.util.logD /** * An adapter that displays search results. + * * @param listener An [SelectableListListener] to bind interactions to. * @author Alexander Capehart (OxygenCobalt) */ class SearchAdapter(private val listener: SelectableListListener) : - SelectionIndicatorAdapter( - ListDiffer.Async(DIFF_CALLBACK)), + SelectionIndicatorAdapter(DIFF_CALLBACK), AuxioRecyclerView.SpanSizeLookup { override fun getItemViewType(position: Int) = diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt index d52b9ef9c..e3733549e 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * SearchEngine.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 @@ -29,11 +30,13 @@ import org.oxycblt.auxio.music.Song /** * Implements the fuzzy-ish searching algorithm used in the search view. + * * @author Alexander Capehart */ interface SearchEngine { /** * Begin a search. + * * @param items The items to search over. * @param query The query to search for. * @return A list of items filtered by the given query. @@ -42,6 +45,7 @@ interface SearchEngine { /** * Input/output data to use with [SearchEngine]. + * * @param songs A list of [Song]s, null if empty. * @param albums A list of [Album]s, null if empty. * @param artists A list of [Artist]s, null if empty. @@ -66,11 +70,12 @@ class SearchEngineImpl @Inject constructor(@ApplicationContext private val conte /** * Search a given [Music] list. + * * @param query The query to search for. The routine will compare this query to the names of - * each object in the list and + * each object in the list and * @param fallback Additional comparison code to run if the item does not match the query - * initially. This can be used to compare against additional attributes to improve search result - * quality. + * initially. This can be used to compare against additional attributes to improve search + * result quality. */ private inline fun List.searchListImpl( query: String, @@ -97,7 +102,7 @@ class SearchEngineImpl @Inject constructor(@ApplicationContext private val conte // which // could make it match the query. val normalizedName = - NORMALIZATION_SANITIZE_REGEX.replace( + NORMALIZE_POST_PROCESSING_REGEX.replace( Normalizer.normalize(name, Normalizer.Form.NFKD), "") if (normalizedName.contains(query, ignoreCase = true)) { return@filter true @@ -110,8 +115,9 @@ class SearchEngineImpl @Inject constructor(@ApplicationContext private val conte private companion object { /** * Converts the output of [Normalizer] to remove any junk characters added by it's - * replacements. + * replacements, alongside punctuation. */ - val NORMALIZATION_SANITIZE_REGEX = Regex("\\p{InCombiningDiacriticalMarks}+") + val NORMALIZE_POST_PROCESSING_REGEX = + Regex("(\\p{InCombiningDiacriticalMarks}+)|(\\p{Punct})") } } 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 caf9c57ad..35ff34079 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * SearchFragment.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 @@ -34,7 +35,6 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentSearchBinding import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment -import org.oxycblt.auxio.list.adapter.BasicListInstructions import org.oxycblt.auxio.list.selection.SelectionViewModel import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist @@ -49,11 +49,10 @@ import org.oxycblt.auxio.util.* /** * The [ListFragment] providing search functionality for the music library. * - * TODO: Better keyboard management - * - * TODO: Multi-filtering with chips - * * @author Alexander Capehart (OxygenCobalt) + * + * TODO: Better keyboard management + * TODO: Multi-filtering with chips */ @AndroidEntryPoint class SearchFragment : ListFragment() { @@ -99,7 +98,7 @@ class SearchFragment : ListFragment() { binding.searchEditText.apply { addTextChangedListener { text -> // Run the search with the updated text as the query - searchModel.search(text?.toString()) + searchModel.search(text?.toString()?.trim()) } if (!launchedKeyboard) { @@ -116,7 +115,7 @@ class SearchFragment : ListFragment() { collectImmediately(searchModel.searchResults, ::updateSearchResults) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) - collect(navModel.exploreNavigationItem, ::handleNavigation) + collect(navModel.exploreNavigationItem.flow, ::handleNavigation) collectImmediately(selectionModel.selected, ::updateSelection) } @@ -164,7 +163,7 @@ class SearchFragment : ListFragment() { // Don't show the RecyclerView (and it's stray overscroll effects) when there // are no results. binding.searchRecycler.isInvisible = results.isEmpty() - searchAdapter.submitList(results.toMutableList(), BasicListInstructions.DIFF) { + searchAdapter.update(results.toMutableList(), null) { // I would make it so that the position is only scrolled back to the top when // the query actually changes instead of once every re-creation event, but sadly // that doesn't seem possible. @@ -187,7 +186,7 @@ class SearchFragment : ListFragment() { } // Keyboard is no longer needed. hideKeyboard() - findNavController().navigate(action) + findNavController().navigateSafe(action) } private fun updateSelection(selected: List) { @@ -201,6 +200,7 @@ class SearchFragment : ListFragment() { /** * Safely focus the keyboard on a particular [View]. + * * @param view The [View] to focus the keyboard on. */ private fun showKeyboard(view: View) { 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 dd973e751..18c857ba4 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchModule.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * SearchModule.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 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 edd22439f..16edab48b 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchSettings.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * SearchSettings.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 @@ -27,6 +28,7 @@ import org.oxycblt.auxio.settings.Settings /** * User configuration specific to the search UI. + * * @author Alexander Capehart (OxygenCobalt) */ interface SearchSettings : Settings { 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 6fdf615fd..4af58703c 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * SearchViewModel.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 @@ -38,6 +39,7 @@ import org.oxycblt.auxio.util.logD /** * An [ViewModel] that keeps performs search operations and tracks their results. + * * @author Alexander Capehart (OxygenCobalt) */ @HiltViewModel @@ -80,6 +82,7 @@ constructor( /** * Asynchronously search the music library. Results will be pushed to [searchResults]. Will * cancel any previous search operations started prior. + * * @param query The query to search the music library for. */ fun search(query: String?) { @@ -142,6 +145,7 @@ constructor( /** * Returns the ID of the filter option to currently highlight. + * * @return A menu item ID of the filtering option selected. */ @IdRes @@ -157,6 +161,7 @@ constructor( /** * Update the filter mode with the newly-selected filter option. + * * @return A menu item ID of the new filtering option selected. */ fun setFilterOptionId(@IdRes id: Int) { diff --git a/app/src/main/java/org/oxycblt/auxio/service/ForegroundManager.kt b/app/src/main/java/org/oxycblt/auxio/service/ForegroundManager.kt index cebb96001..b23457d48 100644 --- a/app/src/main/java/org/oxycblt/auxio/service/ForegroundManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/service/ForegroundManager.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * ForegroundManager.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 @@ -23,6 +24,7 @@ import org.oxycblt.auxio.util.logD /** * A utility to create consistent foreground behavior for a given [Service]. + * * @param service [Service] to wrap in this instance. * @author Alexander Capehart (OxygenCobalt) * @@ -38,8 +40,9 @@ class ForegroundManager(private val service: Service) { /** * Try to enter a foreground state. + * * @param notification The [ForegroundServiceNotification] to show in order to signal the - * foreground state. + * foreground state. * @return true if the state was changed, false otherwise * @see Service.startForeground */ @@ -57,6 +60,7 @@ class ForegroundManager(private val service: Service) { /** * Try to exit a foreground state. Will remove the foreground notification. + * * @return true if the state was changed, false otherwise * @see Service.stopForeground */ diff --git a/app/src/main/java/org/oxycblt/auxio/service/ForegroundServiceNotification.kt b/app/src/main/java/org/oxycblt/auxio/service/ForegroundServiceNotification.kt index c42d126a6..7bcd6118a 100644 --- a/app/src/main/java/org/oxycblt/auxio/service/ForegroundServiceNotification.kt +++ b/app/src/main/java/org/oxycblt/auxio/service/ForegroundServiceNotification.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * ForegroundServiceNotification.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 @@ -26,6 +27,7 @@ import androidx.core.app.NotificationManagerCompat /** * Wrapper around [NotificationCompat.Builder] intended for use for [NotificationCompat]s that * signal a Service's ongoing foreground state. + * * @author Alexander Capehart (OxygenCobalt) */ abstract class ForegroundServiceNotification(context: Context, info: ChannelInfo) : @@ -47,6 +49,7 @@ abstract class ForegroundServiceNotification(context: Context, info: ChannelInfo /** * The code used to identify this notification. + * * @see NotificationManagerCompat.notify */ abstract val code: Int @@ -60,6 +63,7 @@ abstract class ForegroundServiceNotification(context: Context, info: ChannelInfo /** * Reduced representation of a [NotificationChannelCompat]. + * * @param id The ID of the channel. * @param nameRes A string resource ID corresponding to the human-readable name of this channel. */ 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 c5a01cfb5..0bda7bd9d 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * AboutFragment.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 @@ -42,6 +43,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat /** * A [ViewBindingFragment] that displays information about the app and the current music library. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint @@ -93,6 +95,7 @@ class AboutFragment : ViewBindingFragment() { /** * Open the given URI in a web browser. + * * @param uri The URL to open. */ private fun openLinkInBrowser(uri: String) { @@ -116,6 +119,7 @@ class AboutFragment : ViewBindingFragment() { // not work in all cases, especially when no default app was set. If that is the // case, we will try to manually handle these cases before we try to launch the // browser. + @Suppress("DEPRECATION") val pkgName = context.packageManager .resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY) @@ -143,6 +147,7 @@ class AboutFragment : ViewBindingFragment() { /** * Open an app chooser for a given [Intent]. + * * @param intent The [Intent] to show an app chooser for. */ private fun openAppChooser(intent: Intent) { diff --git a/app/src/main/java/org/oxycblt/auxio/settings/BasePreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/BasePreferenceFragment.kt index 547c245e3..3579a4a5d 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/BasePreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/BasePreferenceFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * BasePreferenceFragment.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 @@ -41,18 +42,21 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat /** * Shared [PreferenceFragmentCompat] used across all preference screens. + * * @author Alexander Capehart (OxygenCobalt) */ abstract class BasePreferenceFragment(@XmlRes private val screen: Int) : PreferenceFragmentCompat() { /** * Called when the UI entry of a given [Preference] needs to be configured. + * * @param preference The [Preference] to configure. */ open fun onSetupPreference(preference: Preference) {} /** * Called when an arbitrary [WrappedDialogPreference] needs to be opened. + * * @param preference The [WrappedDialogPreference] to open. */ open fun onOpenDialogPreference(preference: WrappedDialogPreference) {} diff --git a/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt index bc4622dc1..0467abcda 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/RootPreferenceFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * RootPreferenceFragment.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 @@ -29,10 +30,12 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.settings.ui.WrappedDialogPreference +import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.showToast /** * The [PreferenceFragmentCompat] that displays the root settings list. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint @@ -61,19 +64,20 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) { // do one. when (preference.key) { getString(R.string.set_key_ui) -> { - findNavController().navigate(RootPreferenceFragmentDirections.goToUiPreferences()) + findNavController() + .navigateSafe(RootPreferenceFragmentDirections.goToUiPreferences()) } getString(R.string.set_key_personalize) -> { findNavController() - .navigate(RootPreferenceFragmentDirections.goToPersonalizePreferences()) + .navigateSafe(RootPreferenceFragmentDirections.goToPersonalizePreferences()) } getString(R.string.set_key_music) -> { findNavController() - .navigate(RootPreferenceFragmentDirections.goToMusicPreferences()) + .navigateSafe(RootPreferenceFragmentDirections.goToMusicPreferences()) } getString(R.string.set_key_audio) -> { findNavController() - .navigate(RootPreferenceFragmentDirections.goToAudioPreferences()) + .navigateSafe(RootPreferenceFragmentDirections.goToAudioPreferences()) } getString(R.string.set_key_reindex) -> musicModel.refresh() getString(R.string.set_key_rescan) -> musicModel.rescan() diff --git a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt index 5bc4448b9..36594b2de 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * Settings.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 @@ -27,11 +28,13 @@ import org.oxycblt.auxio.util.unlikelyToBeNull /** * Abstract user configuration information. This interface has no functionality whatsoever. Concrete * implementations should be preferred instead. + * * @author Alexander Capehart (OxygenCobalt) */ interface Settings { /** * Migrate any settings fields from older versions into their new counterparts. + * * @throws NotImplementedError If there is nothing to migrate. */ fun migrate() { @@ -40,18 +43,21 @@ interface Settings { /** * Add a listener to monitor for settings updates. Will do nothing if + * * @param listener The listener to add. */ fun registerListener(listener: L) /** * Unregister a listener, preventing any further settings updates from being sent to it. + * * @param listener The listener to unregister, must be the same as the current listener. */ fun unregisterListener(listener: L) /** * A framework-backed [Settings] implementation. + * * @param context [Context] required. */ abstract class Impl(private val context: Context) : @@ -91,6 +97,7 @@ interface Settings { /** * Called when a setting entry with the given [key] has changed. + * * @param key The key of the changed setting. * @param listener The implementation's listener that updates should be applied to. */ diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt index 52506caf5..49eda0656 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/AudioPreferenceFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * AudioPreferenceFragment.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,16 +22,18 @@ import androidx.navigation.fragment.findNavController import org.oxycblt.auxio.R import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.ui.WrappedDialogPreference +import org.oxycblt.auxio.util.navigateSafe /** * Audio settings interface. + * * @author Alexander Capehart (OxygenCobalt) */ class AudioPreferenceFragment : BasePreferenceFragment(R.xml.preferences_audio) { override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_pre_amp)) { - findNavController().navigate(AudioPreferenceFragmentDirections.goToPreAmpDialog()) + findNavController().navigateSafe(AudioPreferenceFragmentDirections.goToPreAmpDialog()) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt index 1ee5e05e6..8e60a1b33 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * MusicPreferenceFragment.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 @@ -25,9 +26,11 @@ import javax.inject.Inject import org.oxycblt.auxio.R import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.ui.WrappedDialogPreference +import org.oxycblt.auxio.util.navigateSafe /** * "Content" settings. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint @@ -36,7 +39,8 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music) override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_separators)) { - findNavController().navigate(MusicPreferenceFragmentDirections.goToSeparatorsDialog()) + findNavController() + .navigateSafe(MusicPreferenceFragmentDirections.goToSeparatorsDialog()) } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt index 73c5147ec..8669c52c3 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/PersonalizePreferenceFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * PersonalizePreferenceFragment.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,15 +22,18 @@ import androidx.navigation.fragment.findNavController import org.oxycblt.auxio.R import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.ui.WrappedDialogPreference +import org.oxycblt.auxio.util.navigateSafe /** * Personalization settings interface. + * * @author Alexander Capehart (OxygenCobalt) */ class PersonalizePreferenceFragment : BasePreferenceFragment(R.xml.preferences_personalize) { override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_home_tabs)) { - findNavController().navigate(PersonalizePreferenceFragmentDirections.goToTabDialog()) + findNavController() + .navigateSafe(PersonalizePreferenceFragmentDirections.goToTabDialog()) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt index 4e564e9d5..b1105f123 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/UIPreferenceFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * UIPreferenceFragment.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 @@ -27,9 +28,11 @@ import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.ui.WrappedDialogPreference import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.isNight +import org.oxycblt.auxio.util.navigateSafe /** * Display preferences. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint @@ -38,7 +41,7 @@ class UIPreferenceFragment : BasePreferenceFragment(R.xml.preferences_ui) { override fun onOpenDialogPreference(preference: WrappedDialogPreference) { if (preference.key == getString(R.string.set_key_accent)) { - findNavController().navigate(UIPreferenceFragmentDirections.goToAccentDialog()) + findNavController().navigateSafe(UIPreferenceFragmentDirections.goToAccentDialog()) } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreference.kt b/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreference.kt index 1289e121e..bffb5f746 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreference.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreference.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * IntListPreference.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 @@ -114,8 +115,9 @@ constructor( /** * Get the index of the current value. + * * @return The index of the current value within [values], or -1 if the [IntListPreference] is - * not set. + * not set. */ fun getValueIndex(): Int { val curValue = currentValue @@ -127,6 +129,7 @@ constructor( /** * Set the current value of this preference using it's index. + * * @param index The index of the new value within [values]. Must be valid. */ fun setValueIndex(index: Int) { 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 5613e1ef3..2995b6353 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * IntListPreferenceDialog.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 @@ -28,6 +29,7 @@ import org.oxycblt.auxio.util.fixDoubleRipple /** * The companion dialog to [IntListPreference]. Use [from] to create an instance. + * * @author Alexander Capehart (OxygenCobalt) */ class IntListPreferenceDialog : PreferenceDialogFragmentCompat() { @@ -72,6 +74,7 @@ class IntListPreferenceDialog : PreferenceDialogFragmentCompat() { /** * Create a new instance. + * * @param preference The [IntListPreference] to display. * @return A new instance. */ diff --git a/app/src/main/java/org/oxycblt/auxio/settings/ui/PreferenceHeaderItemDecoration.kt b/app/src/main/java/org/oxycblt/auxio/settings/ui/PreferenceHeaderItemDecoration.kt index 5ab4b0ebf..10fe7f13e 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/ui/PreferenceHeaderItemDecoration.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/ui/PreferenceHeaderItemDecoration.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * PreferenceHeaderItemDecoration.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 @@ -30,6 +31,7 @@ import org.oxycblt.auxio.R /** * A [BackportMaterialDividerItemDecoration] that sets up the divider configuration to correctly * separate preference categories. + * * @author Alexander Capehart (OxygenCobalt) */ class PreferenceHeaderItemDecoration diff --git a/app/src/main/java/org/oxycblt/auxio/settings/ui/WrappedDialogPreference.kt b/app/src/main/java/org/oxycblt/auxio/settings/ui/WrappedDialogPreference.kt index ec5317b4e..f73e10245 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/ui/WrappedDialogPreference.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/ui/WrappedDialogPreference.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * WrappedDialogPreference.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 @@ -24,6 +25,7 @@ import androidx.preference.DialogPreference /** * Wraps a [DialogPreference] to be instantiatable. This has no purpose other to ensure that custom * dialog preferences are handled. + * * @author Alexander Capehart (OxygenCobalt) */ class WrappedDialogPreference 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 aefcedd1e..d0cafd9f0 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/BaseBottomSheetBehavior.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * BaseBottomSheetBehavior.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 @@ -34,6 +35,7 @@ import org.oxycblt.auxio.util.systemGestureInsetsCompat * 1. No reasonable edge-to-edge support. * 2. Strange corner radius behaviors. * 3. Inability to skip half-expanded state when full-screen. + * * @author Alexander Capehart (OxygenCobalt) */ abstract class BaseBottomSheetBehavior(context: Context, attributeSet: AttributeSet?) : @@ -48,6 +50,7 @@ abstract class BaseBottomSheetBehavior(context: Context, attributeSet: /** * Create a background [Drawable] to use for this [BaseBottomSheetBehavior]'s child [View]. + * * @param context [Context] that can be used to draw the [Drawable]. * @return A background drawable. */ @@ -56,6 +59,7 @@ abstract class BaseBottomSheetBehavior(context: Context, attributeSet: /** * Called when window insets are being applied to the [View] this [BaseBottomSheetBehavior] is * linked to. + * * @param child The child view receiving the [WindowInsets]. * @param insets The [WindowInsets] to apply. * @return The (possibly modified) [WindowInsets]. diff --git a/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt b/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt index 77f1cfc01..eb3560271 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * BottomSheetContentBehavior.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 @@ -32,6 +33,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat * A behavior that automatically re-layouts and re-insets content to align with the parent layout's * bottom sheet. Ideally, we would only want to re-inset content, but that has too many issues to * sensibly implement. + * * @author Alexander Capehart (OxygenCobalt) */ class BottomSheetContentBehavior(context: Context, attributeSet: AttributeSet?) : diff --git a/app/src/main/java/org/oxycblt/auxio/ui/CoordinatorAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/ui/CoordinatorAppBarLayout.kt index 3a14709b5..002f49868 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/CoordinatorAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/CoordinatorAppBarLayout.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * CoordinatorAppBarLayout.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 @@ -110,6 +111,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr * An [AppBarLayout.OnOffsetChangedListener] that will automatically move the given * [RecyclerView] as the [AppBarLayout] expands. Should be added right when the view is * expanding. Will be removed automatically. + * * @param recycler [RecyclerView] to scroll with the [AppBarLayout]. */ private class ExpansionHackListener(private val recycler: RecyclerView) : diff --git a/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt b/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt index becc077ad..116f57013 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * NavigationViewModel.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 @@ -19,81 +20,75 @@ package org.oxycblt.auxio.ui import androidx.lifecycle.ViewModel import androidx.navigation.NavDirections -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.util.Event +import org.oxycblt.auxio.util.MutableEvent import org.oxycblt.auxio.util.logD /** A [ViewModel] that handles complicated navigation functionality. */ class NavigationViewModel : ViewModel() { - private val _mainNavigationAction = MutableStateFlow(null) + private val _mainNavigationAction = MutableEvent() /** * Flag for navigation within the main navigation graph. Only intended for use by MainFragment. */ - val mainNavigationAction: StateFlow + val mainNavigationAction: Event get() = _mainNavigationAction - private val _exploreNavigationItem = MutableStateFlow(null) + private val _exploreNavigationItem = MutableEvent() /** * Flag for navigation within the explore navigation graph. Observe this to coordinate * navigation to a specific [Music] item. */ - val exploreNavigationItem: StateFlow + val exploreNavigationItem: Event get() = _exploreNavigationItem - private val _exploreArtistNavigationItem = MutableStateFlow(null) + private val _exploreArtistNavigationItem = MutableEvent() /** * Variation of [exploreNavigationItem] for situations where the choice of parent [Artist] to * navigate to is ambiguous. Only intended for use by MainFragment, as the resolved choice will * eventually be assigned to [exploreNavigationItem]. */ - val exploreArtistNavigationItem: StateFlow + val exploreArtistNavigationItem: Event get() = _exploreArtistNavigationItem /** * Navigate to something in the main navigation graph. This can be used by UIs in the explore * navigation graph to trigger navigation in the higher-level main navigation graph. Will do * nothing if already navigating. + * * @param action The [MainNavigationAction] to perform. */ fun mainNavigateTo(action: MainNavigationAction) { - if (_mainNavigationAction.value != null) { + if (_mainNavigationAction.flow.value != null) { logD("Already navigating, not doing main action") return } logD("Navigating with action $action") - _mainNavigationAction.value = action - } - - /** - * Mark that the navigation process within the main navigation graph (initiated by - * [mainNavigateTo]) was completed. - */ - fun finishMainNavigation() { - logD("Finishing main navigation process") - _mainNavigationAction.value = null + _mainNavigationAction.put(action) } /** * Navigate to a given [Music] item. Will do nothing if already navigating. + * * @param music The [Music] to navigate to. */ fun exploreNavigateTo(music: Music) { - if (_exploreNavigationItem.value != null) { + if (_exploreNavigationItem.flow.value != null) { logD("Already navigating, not doing explore action") return } logD("Navigating to ${music.rawName}") - _exploreNavigationItem.value = music + _exploreNavigationItem.put(music) } /** * Navigate to one of the parent [Artist]'s of the given [Song]. + * * @param song The [Song] to navigate with. If there are multiple parent [Artist]s, a picker - * dialog will be shown. + * dialog will be shown. */ fun exploreNavigateToParentArtist(song: Song) { exploreNavigateToParentArtistImpl(song, song.artists) @@ -101,15 +96,16 @@ class NavigationViewModel : ViewModel() { /** * Navigate to one of the parent [Artist]'s of the given [Album]. + * * @param album The [Album] to navigate with. If there are multiple parent [Artist]s, a picker - * dialog will be shown. + * dialog will be shown. */ fun exploreNavigateToParentArtist(album: Album) { exploreNavigateToParentArtistImpl(album, album.artists) } private fun exploreNavigateToParentArtistImpl(item: Music, artists: List) { - if (_exploreArtistNavigationItem.value != null) { + if (_exploreArtistNavigationItem.flow.value != null) { logD("Already navigating, not doing explore action") return } @@ -118,25 +114,16 @@ class NavigationViewModel : ViewModel() { exploreNavigateTo(artists[0]) } else { logD("Navigating to a choice of ${artists.map { it.rawName }}") - _exploreArtistNavigationItem.value = item + _exploreArtistNavigationItem.put(item) } } - - /** - * Mark that the navigation process within the explore navigation graph (initiated by - * [exploreNavigateTo]) was completed. - */ - fun finishExploreNavigation() { - logD("Finishing explore navigation process") - _exploreNavigationItem.value = null - _exploreArtistNavigationItem.value = null - } } /** * Represents the possible actions within the main navigation graph. This can be used with * [NavigationViewModel] to initiate navigation in the main navigation graph from anywhere in the * app, including outside the main navigation graph. + * * @author Alexander Capehart (OxygenCobalt) */ sealed class MainNavigationAction { @@ -148,8 +135,9 @@ sealed class MainNavigationAction { /** * Navigate to the given [NavDirections]. + * * @param directions The [NavDirections] to navigate to. Assumed to be part of the main - * navigation graph. + * navigation graph. */ data class Directions(val directions: NavDirections) : MainNavigationAction() } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/RippleFixMaterialButton.kt b/app/src/main/java/org/oxycblt/auxio/ui/RippleFixMaterialButton.kt index 514858f63..c1d1074a9 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/RippleFixMaterialButton.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/RippleFixMaterialButton.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * RippleFixMaterialButton.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 @@ -27,6 +28,7 @@ import org.oxycblt.auxio.util.fixDoubleRipple /** * Fixes an issue where double ripples appear on [MaterialButton] from AppCompat 1.5 afterwards due * to a currently unfixed change. + * * @author Alexander Capehart (OxygenCobalt) */ open class RippleFixMaterialButton diff --git a/app/src/main/java/org/oxycblt/auxio/ui/UIModule.kt b/app/src/main/java/org/oxycblt/auxio/ui/UIModule.kt index 335459c17..4f7856208 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/UIModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/UIModule.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * UIModule.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 diff --git a/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt b/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt index 10f8bbc52..13bcbcbf9 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Auxio Project + * UISettings.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 @@ -30,6 +31,7 @@ import org.oxycblt.auxio.util.logD /** * User configuration for the general app UI. + * * @author Alexander Capehart (OxygenCobalt) */ interface UISettings : Settings { diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt index 1b9a5eb47..1615cbfd3 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * ViewBindingDialogFragment.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 @@ -32,6 +33,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull /** * A lifecycle-aware [DialogFragment] that automatically manages the [ViewBinding] lifecycle. + * * @author Alexander Capehart (OxygenCobalt) */ abstract class ViewBindingDialogFragment : DialogFragment() { @@ -39,6 +41,7 @@ abstract class ViewBindingDialogFragment : DialogFragment() { /** * Configure the [AlertDialog.Builder] during [onCreateDialog]. + * * @param builder The [AlertDialog.Builder] to configure. * @see onCreateDialog */ @@ -46,6 +49,7 @@ abstract class ViewBindingDialogFragment : DialogFragment() { /** * Inflate the [ViewBinding] during [onCreateView]. + * * @param inflater The [LayoutInflater] to inflate the [ViewBinding] with. * @return A new [ViewBinding] instance. * @see onCreateView @@ -54,6 +58,7 @@ abstract class ViewBindingDialogFragment : DialogFragment() { /** * Configure the newly-inflated [ViewBinding] during [onViewCreated]. + * * @param binding The [ViewBinding] to configure. * @param savedInstanceState The previously saved state of the UI. * @see onViewCreated @@ -62,6 +67,7 @@ abstract class ViewBindingDialogFragment : DialogFragment() { /** * Free memory held by the [ViewBinding] during [onDestroyView] + * * @param binding The [ViewBinding] to release. * @see onDestroyView */ @@ -73,6 +79,7 @@ abstract class ViewBindingDialogFragment : DialogFragment() { /** * Get the [ViewBinding] under the assumption that it has been inflated. + * * @return The currently-inflated [ViewBinding]. * @throws IllegalStateException if the [ViewBinding] is not inflated. */ diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt index aaaf3119e..c3a493bb5 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * ViewBindingFragment.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 @@ -28,6 +29,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull /** * A fragment enabling ViewBinding inflation and usage across the fragment lifecycle. + * * @author Alexander Capehart (OxygenCobalt) */ abstract class ViewBindingFragment : Fragment() { @@ -35,6 +37,7 @@ abstract class ViewBindingFragment : Fragment() { /** * Inflate the [ViewBinding] during [onCreateView]. + * * @param inflater The [LayoutInflater] to inflate the [ViewBinding] with. * @return A new [ViewBinding] instance. * @see onCreateView @@ -43,6 +46,7 @@ abstract class ViewBindingFragment : Fragment() { /** * Configure the newly-inflated [ViewBinding] during [onViewCreated]. + * * @param binding The [ViewBinding] to configure. * @param savedInstanceState The previously saved state of the UI. * @see onViewCreated @@ -51,6 +55,7 @@ abstract class ViewBindingFragment : Fragment() { /** * Free memory held by the [ViewBinding] during [onDestroyView] + * * @param binding The [ViewBinding] to release. * @see onDestroyView */ @@ -62,6 +67,7 @@ abstract class ViewBindingFragment : Fragment() { /** * Get the [ViewBinding] under the assumption that it has been inflated. + * * @return The currently-inflated [ViewBinding]. * @throws IllegalStateException if the [ViewBinding] is not inflated. */ 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 cfa6525bd..de03d3ba9 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * Accent.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 @@ -134,9 +135,10 @@ class Accent private constructor(val index: Int) { companion object { /** * Create a new instance. + * * @param index The unique number for this particular accent. * @return A new [Accent] with the specified [index]. If [index] is not within the range of - * valid accents, [index] will be [DEFAULT] instead. + * valid accents, [index] will be [DEFAULT] instead. */ fun from(index: Int): Accent { if (index !in 0 until MAX) { diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt index 46e7e66fc..09eb411ef 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * AccentAdapter.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 @@ -30,6 +31,7 @@ import org.oxycblt.auxio.util.inflater /** * A [RecyclerView.Adapter] that displays [Accent] choices. + * * @param listener A [ClickableListListener] to bind interactions to. * @author Alexander Capehart (OxygenCobalt) */ @@ -62,6 +64,7 @@ class AccentAdapter(private val listener: ClickableListListener) : /** * Update the currently selected [Accent]. + * * @param accent The new [Accent] to select. */ fun setSelectedAccent(accent: Accent) { @@ -83,6 +86,7 @@ class AccentAdapter(private val listener: ClickableListListener) : /** * A [RecyclerView.ViewHolder] that displays an [Accent] choice. Use [from] to create an instance. + * * @author Alexander Capehart (OxygenCobalt) */ class AccentViewHolder private constructor(private val binding: ItemAccentBinding) : @@ -90,6 +94,7 @@ class AccentViewHolder private constructor(private val binding: ItemAccentBindin /** * Bind new data to this instance. + * * @param accent The new [Accent] to bind. * @param listener A [ClickableListListener] to bind interactions to. */ @@ -106,6 +111,7 @@ class AccentViewHolder private constructor(private val binding: ItemAccentBindin /** * Set whether this [Accent] is selected or not. + * * @param isSelected Whether this [Accent] is currently selected. */ fun setSelected(isSelected: Boolean) { @@ -122,6 +128,7 @@ class AccentViewHolder private constructor(private val binding: ItemAccentBindin companion object { /** * Create a new instance. + * * @param parent The parent to inflate this instance from. * @return A new instance. */ diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt index 1c9a042f5..a09e0f0d5 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * AccentCustomizeDialog.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 @@ -34,6 +35,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull /** * A [ViewBindingDialogFragment] that allows the user to configure the current [Accent]. + * * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentGridLayoutManager.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentGridLayoutManager.kt index 6b075aaf2..d4fa6c00c 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentGridLayoutManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentGridLayoutManager.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * AccentGridLayoutManager.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 diff --git a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt index 85aa256da..d4c058077 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * ContextUtil.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 @@ -43,6 +44,7 @@ import org.oxycblt.auxio.R /** * Get a [LayoutInflater] instance from this [Context]. + * * @see LayoutInflater.from */ val Context.inflater: LayoutInflater @@ -60,6 +62,7 @@ val Context.isLandscape /** * Concatenate values in a list together in a localized manner. + * * @param context [Context] require.d * @param map Function to map the [T] values to a string value to be concatenated. */ @@ -78,23 +81,24 @@ inline fun List.concatLocalized(context: Context, map: (T) -> String): St } /** - * @brief Get a plural resource. * @param pluralRes A plural resource ID. * @param value Int value for the plural. * @return The formatted string requested. + * @brief Get a plural resource. */ fun Context.getPlural(@PluralsRes pluralRes: Int, value: Int) = resources.getQuantityString(pluralRes, value, value) /** - * @brief Get an integer resource. * @param integerRes An integer resource ID. * @return The integer resource requested. + * @brief Get an integer resource. */ fun Context.getInteger(@IntegerRes integerRes: Int) = resources.getInteger(integerRes) /** * Get a [ColorStateList] resource. + * * @param colorRes A color resource ID. * @return The [ColorStateList] requested. */ @@ -105,6 +109,7 @@ fun Context.getColorCompat(@ColorRes colorRes: Int) = /** * Get a [ColorStateList] pointed to by an attribute. + * * @param attrRes An attribute resource ID. * @return The [ColorStateList] the requested attribute points to. */ @@ -126,6 +131,7 @@ fun Context.getAttrColorCompat(@AttrRes attrRes: Int): ColorStateList { /** * Get a Drawable. + * * @param drawableRes The Drawable resource ID. * @return The Drawable requested. */ @@ -136,6 +142,7 @@ fun Context.getDrawableCompat(@DrawableRes drawableRes: Int) = /** * Get the complex (i.e DP) size of a dimension. + * * @param dimenRes The dimension resource. * @return The size of the dimension requested, in complex units. */ @@ -143,6 +150,7 @@ fun Context.getDrawableCompat(@DrawableRes drawableRes: Int) = /** * Get the pixel size of a dimension. + * * @param dimenRes The dimension resource * @return The size of the dimension requested, in pixels */ @@ -150,6 +158,7 @@ fun Context.getDrawableCompat(@DrawableRes drawableRes: Int) = /** * Get an instance of the requested system service. + * * @param T The system service in question. * @param serviceClass The service's kotlin class [Java class will be used in function call] * @return The system service @@ -162,6 +171,7 @@ fun Context.getSystemServiceCompat(serviceClass: KClass) = /** * Create a short-length [Toast] with text from the specified string resource. + * * @param stringRes The resource to the string to use in the toast. */ fun Context.showToast(@StringRes stringRes: Int) { @@ -178,6 +188,7 @@ fun Context.newMainPendingIntent(): PendingIntent = /** * Create a [PendingIntent] that will broadcast the specified command when launched. + * * @param action The action to broadcast when the [PendingIntent] is launched. */ fun Context.newBroadcastPendingIntent(action: String): PendingIntent = 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 e81dc11d3..9ec0caa75 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * FrameworkUtil.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 @@ -28,25 +29,20 @@ import androidx.appcompat.widget.AppCompatButton import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.graphics.Insets import androidx.core.graphics.drawable.DrawableCompat -import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.NavController +import androidx.navigation.NavDirections import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.launch /** * Get if this [View] contains the given [PointF], with optional leeway. + * * @param x The x value of the point to check. * @param y The y value of the point to check. * @param minTouchTargetSize A minimum size to use when checking the value. This can be used to - * extend the range where a point is considered "contained" by the [View] beyond it's actual size. + * extend the range where a point is considered "contained" by the [View] beyond it's actual size. * @return true if the [PointF] is contained by the view, false otherwise. Adapted from - * AndroidFastScroll: https://github.com/zhanghai/AndroidFastScroll + * AndroidFastScroll: https://github.com/zhanghai/AndroidFastScroll */ fun View.isUnder(x: Float, y: Float, minTouchTargetSize: Int = 0) = isUnderImpl(x, left, right, (parent as View).width, minTouchTargetSize) && @@ -54,6 +50,7 @@ fun View.isUnder(x: Float, y: Float, minTouchTargetSize: Int = 0) = /** * Internal implementation of [isUnder]. + * * @param position The position to check. * @param viewStart The start of the view bounds, on the same axis as [position]. * @param viewEnd The end of the view bounds, on the same axis as [position] @@ -118,6 +115,19 @@ fun AppCompatButton.fixDoubleRipple() { } } +/** + * Crash-safe wrapped around [NavController.navigate] that will not crash if multiple destinations + * are selected at once. + * + * @param directions The [NavDirections] to navigate with. + */ +fun NavController.navigateSafe(directions: NavDirections) = + try { + navigate(directions) + } catch (e: IllegalStateException) { + // Nothing to do. + } + /** * Get the [CoordinatorLayout.Behavior] of a [View], or null if the [View] is not part of a * [CoordinatorLayout] or does not have a [CoordinatorLayout.Behavior]. @@ -125,83 +135,6 @@ fun AppCompatButton.fixDoubleRipple() { val View.coordinatorLayoutBehavior: CoordinatorLayout.Behavior? get() = (layoutParams as? CoordinatorLayout.LayoutParams)?.behavior -/** - * Collect a [StateFlow] into [block] in a lifecycle-aware manner *eventually.* Due to co-routine - * launching, the initializing call will occur ~100ms after draw time. If this is not desirable, use - * [collectImmediately]. - * @param stateFlow The [StateFlow] to collect. - * @param block The code to run when the [StateFlow] updates. - */ -fun Fragment.collect(stateFlow: StateFlow, block: (T) -> Unit) { - launch { stateFlow.collect(block) } -} - -/** - * Collect a [StateFlow] into a [block] in a lifecycle-aware manner *immediately.* This will - * immediately run an initializing call to ensure the UI is set up before draw-time. Note that this - * will result in two initializing calls. - * @param stateFlow The [StateFlow] to collect. - * @param block The code to run when the [StateFlow] updates. - */ -fun Fragment.collectImmediately(stateFlow: StateFlow, block: (T) -> Unit) { - block(stateFlow.value) - launch { stateFlow.collect(block) } -} - -/** - * Like [collectImmediately], but with two [StateFlow] instances that are collected with the same - * block. - * @param a The first [StateFlow] to collect. - * @param b The second [StateFlow] to collect. - * @param block The code to run when either [StateFlow] updates. - */ -fun Fragment.collectImmediately( - a: StateFlow, - b: StateFlow, - block: (T1, T2) -> Unit -) { - block(a.value, b.value) - // We can combine flows, but only if we transform them into one flow output. - // Thus, we have to first combine the two flow values into a Pair, and then - // decompose it when we collect the values. - val combine = a.combine(b) { first, second -> Pair(first, second) } - launch { combine.collect { block(it.first, it.second) } } -} - -/** - * Like [collectImmediately], but with three [StateFlow] instances that are collected with the same - * block. - * @param a The first [StateFlow] to collect. - * @param b The second [StateFlow] to collect. - * @param c The third [StateFlow] to collect. - * @param block The code to run when any of the [StateFlow]s update. - */ -fun Fragment.collectImmediately( - a: StateFlow, - b: StateFlow, - c: StateFlow, - block: (T1, T2, T3) -> Unit -) { - block(a.value, b.value, c.value) - val combine = combine(a, b, c) { a1, b2, c3 -> Triple(a1, b2, c3) } - launch { combine.collect { block(it.first, it.second, it.third) } } -} - -/** - * Launch a [Fragment] co-routine whenever the [Lifecycle] hits the given [Lifecycle.State]. This - * should always been used when launching [Fragment] co-routines was it will not result in - * unexpected behavior. - * @param state The [Lifecycle.State] to launch the co-routine in. - * @param block The block to run in the co-routine. - * @see repeatOnLifecycle - */ -private fun Fragment.launch( - state: Lifecycle.State = Lifecycle.State.STARTED, - block: suspend CoroutineScope.() -> Unit -) { - viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(state, block) } -} - /** * Get the "System Bar" [Insets] in this [WindowInsets] instance in a version-compatible manner This * can be used to prevent [View] elements from intersecting with the navigation bars. @@ -231,7 +164,7 @@ val WindowInsets.systemGestureInsetsCompat: Insets // this should allow this code to fall back to system bar insets easily if the system // does not provide system gesture insets. This does require androidx Insets to allow // us to use the max method on all versions however, so we will want to convert the - // system-provided insets to such.. + // system-provided insets to such. when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { // API 30+, use window inset map. @@ -250,6 +183,7 @@ val WindowInsets.systemGestureInsetsCompat: Insets /** * Returns the given [Insets] based on the to the API 30+ [WindowInsets] convention. + * * @param typeMask The type of [Insets] to obtain. * @return Compat [Insets] corresponding to the given type. * @see WindowInsets.getInsets @@ -259,6 +193,7 @@ private fun WindowInsets.getCompatInsets(typeMask: Int) = Insets.toCompatInsets( /** * Returns "System Bar" [Insets] based on the API 21+ [WindowInsets] convention. + * * @return Compat [Insets] consisting of the [WindowInsets] "System Bar" [Insets] field. * @see WindowInsets.getSystemWindowInsets */ @@ -272,6 +207,7 @@ private fun WindowInsets.getSystemWindowCompatInsets() = /** * Returns "System Bar" [Insets] based on the API 29 [WindowInsets] convention. + * * @return Compat [Insets] consisting of the [WindowInsets] "System Gesture" [Insets] fields. * @see WindowInsets.getSystemGestureInsets */ @@ -281,6 +217,7 @@ private fun WindowInsets.getSystemGestureCompatInsets() = Insets.toCompatInsets( /** * Replace the "System Bar" [Insets] in [WindowInsets] with a new set of [Insets]. + * * @param left The new left inset. * @param top The new top inset. * @param right The new right inset. diff --git a/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt index 71b963b38..047c24c21 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * LangUtil.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 @@ -35,6 +36,7 @@ fun unlikelyToBeNull(value: T?) = /** * Require that the given data is a specific type [T]. + * * @param data The data to check. * @return A data casted to [T]. * @throws IllegalStateException If the data cannot be casted to [T]. @@ -46,18 +48,21 @@ inline fun requireIs(data: Any?): T { /** * Aliases a check to ensure that the given number is non-zero. + * * @return The given number if it's non-zero, null otherwise. */ fun Int.nonZeroOrNull() = if (this > 0) this else null /** * Aliases a check to ensure that the given number is non-zero. + * * @return The same number if it's non-zero, null otherwise. */ fun Long.nonZeroOrNull() = if (this > 0) this else null /** * Aliases a check to ensure a given value is in a specified range. + * * @param range The valid range of values for this number. * @return The same number if it is in the range, null otherwise. */ @@ -66,6 +71,7 @@ fun Int.inRangeOrNull(range: IntRange) = if (range.contains(this)) this else nul /** * Lazily set up a reflected field. Automatically handles visibility changes. Adapted from Material * Files: https://github.com/zhanghai/MaterialFiles + * * @param clazz The [KClass] to reflect into. * @param field The name of the field to obtain. */ @@ -75,6 +81,7 @@ fun lazyReflectedField(clazz: KClass<*>, field: String) = lazy { /** * Lazily set up a reflected method. Automatically handles visibility changes. Adapted from Material * Files: https://github.com/zhanghai/MaterialFiles + * * @param clazz The [KClass] to reflect into. * @param method The name of the method to obtain. */ @@ -84,6 +91,7 @@ fun lazyReflectedMethod(clazz: KClass<*>, method: String) = lazy { /** * Convert a [String] to a [UUID]. + * * @return A [UUID] converted from the [String] value, or null if the value was not valid. * @see UUID.fromString */ diff --git a/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt index ee447580d..f7418a61e 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * LogUtil.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 @@ -25,12 +26,14 @@ import org.oxycblt.auxio.BuildConfig /** * Log an object to the debug channel. Automatically handles tags. + * * @param obj The object to log. */ fun Any.logD(obj: Any?) = logD("$obj") /** * Log a string message to the debug channel. Automatically handles tags. + * * @param msg The message to log. */ fun Any.logD(msg: String) { @@ -41,12 +44,14 @@ fun Any.logD(msg: String) { /** * Log a string message to the warning channel. Automatically handles tags. + * * @param msg The message to log. */ fun Any.logW(msg: String) = Log.w(autoTag, msg) /** * Log a string message to the error channel. Automatically handles tags. + * * @param msg The message to log. */ fun Any.logE(msg: String) = Log.e(autoTag, msg) diff --git a/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt new file mode 100644 index 000000000..c1e1a4a92 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2023 Auxio Project + * StateUtil.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.util + +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +/** + * A wrapper around [StateFlow] exposing a one-time consumable event. + * + * @author Alexander Capehart (OxygenCobalt) + */ +interface Event { + /** The inner [StateFlow] contained by the [Event]. */ + val flow: StateFlow + /** + * Consume whatever value is currently contained by this instance. + * + * @return A value placed into this instance prior, or null if there isn't any. + */ + fun consume(): T? +} + +/** + * A wrapper around [StateFlow] exposing a one-time consumable event that can be modified by it's + * owner. + * + * @author Alexander Capehart (OxygenCobalt) + */ +class MutableEvent : Event { + override val flow = MutableStateFlow(null) + override fun consume() = flow.value?.also { flow.value = null } + + /** + * Place a new value into this instance, replacing any prior value. + * + * @param v The value to update with. + */ + fun put(v: T) { + flow.value = v + } +} + +/** + * Collect a [StateFlow] into [block] in a lifecycle-aware manner *eventually.* Due to co-routine + * launching, the initializing call will occur ~100ms after draw time. If this is not desirable, use + * [collectImmediately]. + * + * @param stateFlow The [StateFlow] to collect. + * @param block The code to run when the [StateFlow] updates. + */ +fun Fragment.collect(stateFlow: StateFlow, block: (T) -> Unit) { + launch { stateFlow.collect(block) } +} + +/** + * Collect a [StateFlow] into a [block] in a lifecycle-aware manner *immediately.* This will + * immediately run an initializing call to ensure the UI is set up before draw-time. Note that this + * will result in two initializing calls. + * + * @param stateFlow The [StateFlow] to collect. + * @param block The code to run when the [StateFlow] updates. + */ +fun Fragment.collectImmediately(stateFlow: StateFlow, block: (T) -> Unit) { + block(stateFlow.value) + launch { stateFlow.collect(block) } +} + +/** + * Like [collectImmediately], but with two [StateFlow] instances that are collected with the same + * block. + * + * @param a The first [StateFlow] to collect. + * @param b The second [StateFlow] to collect. + * @param block The code to run when either [StateFlow] updates. + */ +fun Fragment.collectImmediately( + a: StateFlow, + b: StateFlow, + block: (T1, T2) -> Unit +) { + block(a.value, b.value) + // We can combine flows, but only if we transform them into one flow output. + // Thus, we have to first combine the two flow values into a Pair, and then + // decompose it when we collect the values. + val combine = a.combine(b) { first, second -> Pair(first, second) } + launch { combine.collect { block(it.first, it.second) } } +} + +/** + * Like [collectImmediately], but with three [StateFlow] instances that are collected with the same + * block. + * + * @param a The first [StateFlow] to collect. + * @param b The second [StateFlow] to collect. + * @param c The third [StateFlow] to collect. + * @param block The code to run when any of the [StateFlow]s update. + */ +fun Fragment.collectImmediately( + a: StateFlow, + b: StateFlow, + c: StateFlow, + block: (T1, T2, T3) -> Unit +) { + block(a.value, b.value, c.value) + val combine = combine(a, b, c) { a1, b2, c3 -> Triple(a1, b2, c3) } + launch { combine.collect { block(it.first, it.second, it.third) } } +} + +/** + * Launch a [Fragment] co-routine whenever the [Lifecycle] hits the given [Lifecycle.State]. This + * should always been used when launching [Fragment] co-routines was it will not result in + * unexpected behavior. + * + * @param state The [Lifecycle.State] to launch the co-routine in. + * @param block The block to run in the co-routine. + * @see repeatOnLifecycle + */ +private fun Fragment.launch( + state: Lifecycle.State = Lifecycle.State.STARTED, + block: suspend CoroutineScope.() -> Unit +) { + viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(state, block) } +} 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 ebea97289..6069beed8 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * WidgetComponent.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 @@ -41,6 +42,7 @@ import org.oxycblt.auxio.util.logD /** * A component that manages the "Now Playing" state. This is kept separate from the [WidgetProvider] * itself to prevent possible memory leaks and enable extension to more widgets in the future. + * * @author Alexander Capehart (OxygenCobalt) */ class WidgetComponent @@ -91,13 +93,11 @@ constructor( } return if (cornerRadius > 0) { - // If rounded, educe the bitmap size further to obtain more pronounced + // If rounded, reduce the bitmap size further to obtain more pronounced // rounded corners. - builder - .size(getSafeRemoteViewsImageSize(context, 10f)) - .transformations( - SquareFrameTransform.INSTANCE, - RoundedCornersTransformation(cornerRadius.toFloat())) + builder.transformations( + SquareFrameTransform.INSTANCE, + RoundedCornersTransformation(cornerRadius.toFloat())) } else { builder.size(getSafeRemoteViewsImageSize(context)) } @@ -134,6 +134,7 @@ constructor( /** * A condensed form of the playback state that is safe to use in AppWidgets. + * * @param song [Queue.currentSong] * @param cover A pre-loaded album cover [Bitmap] for [song]. * @param isPlaying [PlaybackStateManager.playerState] diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index ca737e8ef..3e9726263 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Auxio Project + * WidgetProvider.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 @@ -38,6 +39,7 @@ import org.oxycblt.auxio.util.* /** * The [AppWidgetProvider] for the "Now Playing" widget. This widget shows the current playback * state alongside actions to control it. + * * @author Alexander Capehart (OxygenCobalt) */ class WidgetProvider : AppWidgetProvider() { @@ -69,6 +71,7 @@ class WidgetProvider : AppWidgetProvider() { /** * Update the currently shown layout based on the given [WidgetComponent.PlaybackState] + * * @param context [Context] required to update the widget layout. * @param uiSettings [UISettings] to obtain round mode configuration * @param state [WidgetComponent.PlaybackState] to show, or null if no playback is going on. @@ -105,6 +108,7 @@ class WidgetProvider : AppWidgetProvider() { /** * Revert to the default layout that displays "No music playing". + * * @param context [Context] required to update the widget layout. */ fun reset(context: Context) { @@ -117,6 +121,7 @@ class WidgetProvider : AppWidgetProvider() { /** * Request an update from [WidgetComponent]. + * * @param context [Context] required to send update request broadcast. */ private fun requestUpdate(context: Context) { @@ -231,6 +236,7 @@ class WidgetProvider : AppWidgetProvider() { /** * Set up the album cover in a [RemoteViews] layout that contains one. + * * @param context [Context] required to set up the view. * @param state Current [WidgetComponent.PlaybackState] to display. */ @@ -257,6 +263,7 @@ class WidgetProvider : AppWidgetProvider() { /** * Set up the album cover, song title, and artist name in a [RemoteViews] layout that contains * them. + * * @param context [Context] required to set up the view. * @param state Current [WidgetComponent.PlaybackState] to display. */ @@ -272,6 +279,7 @@ class WidgetProvider : AppWidgetProvider() { /** * Set up the play/pause button in a [RemoteViews] layout that contains one. + * * @param context [Context] required to set up the view. * @param state Current [WidgetComponent.PlaybackState] to display. */ @@ -308,6 +316,7 @@ class WidgetProvider : AppWidgetProvider() { /** * Set up the play/pause and skip previous/next button in a [RemoteViews] layout that contains * them. + * * @param context [Context] required to set up the view. * @param state Current [WidgetComponent.PlaybackState] to display. */ @@ -333,6 +342,7 @@ class WidgetProvider : AppWidgetProvider() { /** * Set up the play/pause, skip previous/next, and repeat/shuffle buttons in a [RemoteViews] that * contains them. + * * @param context [Context] required to set up the view. * @param state Current [WidgetComponent.PlaybackState] to display. */ diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt index 3a478b6ff..cd7151b13 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2022 Auxio Project + * WidgetUtil.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 @@ -35,6 +36,7 @@ import org.oxycblt.auxio.util.newMainPendingIntent /** * Create a [RemoteViews] instance with the specified layout and an automatic click handler to open * the Auxio activity. + * * @param context [Context] required to create [RemoteViews]. * @param layoutRes Resource ID of the layout to use. Must be compatible with [RemoteViews]. * @return A new [RemoteViews] instance with the specified configuration. @@ -48,9 +50,10 @@ fun newRemoteViews(context: Context, @LayoutRes layoutRes: Int): RemoteViews { /** * Get an image size guaranteed to not exceed the [RemoteViews] bitmap memory limit, assuming that * there is only one image. + * * @param context [Context] required to perform calculation. * @param reduce Optional multiplier to reduce the image size. Recommended value is 2 to avoid - * device-specific variations in memory limit. + * device-specific variations in memory limit. * @return The dimension of a bitmap that can be safely used in [RemoteViews]. */ fun getSafeRemoteViewsImageSize(context: Context, reduce: Float = 2f): Int { @@ -64,6 +67,7 @@ fun getSafeRemoteViewsImageSize(context: Context, reduce: Float = 2f): Int { /** * Set the background resource of a [RemoteViews] View. + * * @param viewId The ID of the view to update. * @param drawableRes The resource ID of the drawable to set the background to. */ @@ -73,6 +77,7 @@ fun RemoteViews.setBackgroundResource(@IdRes viewId: Int, @DrawableRes drawableR /** * Set the layout direction of a [RemoteViews] view. + * * @param viewId The ID of the view to update. * @param layoutDirection The layout direction to apply to the view, */ @@ -83,6 +88,7 @@ fun RemoteViews.setLayoutDirection(@IdRes viewId: Int, layoutDirection: Int) { /** * Update the app widget layouts corresponding to the given [WidgetProvider] [ComponentName] with an * adaptive layout, in a version-compatible manner. + * * @param context [Context] required to backport adaptive layout behavior. * @param component [ComponentName] of the app widget layout to update. * @param views Mapping between different size classes and [RemoteViews] instances. @@ -137,6 +143,7 @@ fun AppWidgetManager.updateAppWidgetCompat( /** * Returns whether rounded UI elements are appropriate for the widget, either based on the current * settings or if the widget has to fit in aesthetically with other widgets. + * * @param [uiSettings] [UISettings] required to obtain round mode configuration. * @return true if to use round mode, false otherwise. */ diff --git a/app/src/main/res/color/sel_m3_switch_thumb.xml b/app/src/main/res/color/sel_m3_switch_thumb.xml deleted file mode 100644 index a2d394958..000000000 --- a/app/src/main/res/color/sel_m3_switch_thumb.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/sel_m3_switch_track.xml b/app/src/main/res/color/sel_m3_switch_track.xml deleted file mode 100644 index 19d48ce76..000000000 --- a/app/src/main/res/color/sel_m3_switch_track.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ui_m3_switch_thumb.xml b/app/src/main/res/drawable/ui_m3_switch_thumb.xml deleted file mode 100644 index 04e5221cf..000000000 --- a/app/src/main/res/drawable/ui_m3_switch_thumb.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ui_m3_switch_track.xml b/app/src/main/res/drawable/ui_m3_switch_track.xml deleted file mode 100644 index f89e5de3a..000000000 --- a/app/src/main/res/drawable/ui_m3_switch_track.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-h600dp/item_detail.xml b/app/src/main/res/layout-h600dp/item_detail_header.xml similarity index 100% rename from app/src/main/res/layout-h600dp/item_detail.xml rename to app/src/main/res/layout-h600dp/item_detail_header.xml diff --git a/app/src/main/res/layout-land/item_detail.xml b/app/src/main/res/layout-land/item_detail_header.xml similarity index 100% rename from app/src/main/res/layout-land/item_detail.xml rename to app/src/main/res/layout-land/item_detail_header.xml diff --git a/app/src/main/res/layout-sw600dp/item_detail.xml b/app/src/main/res/layout-sw600dp/item_detail_header.xml similarity index 100% rename from app/src/main/res/layout-sw600dp/item_detail.xml rename to app/src/main/res/layout-sw600dp/item_detail_header.xml diff --git a/app/src/main/res/layout-sw840dp/item_detail.xml b/app/src/main/res/layout-sw840dp/item_detail_header.xml similarity index 100% rename from app/src/main/res/layout-sw840dp/item_detail.xml rename to app/src/main/res/layout-sw840dp/item_detail_header.xml diff --git a/app/src/main/res/layout/dialog_pre_amp.xml b/app/src/main/res/layout/dialog_pre_amp.xml index af49dd3ea..24ca0a85e 100644 --- a/app/src/main/res/layout/dialog_pre_amp.xml +++ b/app/src/main/res/layout/dialog_pre_amp.xml @@ -17,7 +17,7 @@ android:layout_marginStart="@dimen/spacing_large" android:layout_marginTop="@dimen/spacing_medium" android:text="@string/set_pre_amp_with" - android:textAppearance="@style/TextAppearance.Auxio.TitleMediumLowEmphasis" + android:textAppearance="@style/TextAppearance.Auxio.TitleMedium" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -54,7 +54,7 @@ android:layout_marginStart="@dimen/spacing_large" android:layout_marginTop="@dimen/spacing_medium" android:text="@string/set_pre_amp_without" - android:textAppearance="@style/TextAppearance.Auxio.TitleMediumLowEmphasis" + android:textAppearance="@style/TextAppearance.Auxio.TitleMedium" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/with_tags_slider" /> diff --git a/app/src/main/res/layout/fragment_detail.xml b/app/src/main/res/layout/fragment_detail.xml index d62acc2e2..82a5fc5fa 100644 --- a/app/src/main/res/layout/fragment_detail.xml +++ b/app/src/main/res/layout/fragment_detail.xml @@ -35,6 +35,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" - tools:listitem="@layout/item_detail" /> + tools:listitem="@layout/item_detail_header" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_playback_panel.xml b/app/src/main/res/layout/fragment_playback_panel.xml index 01fa91f55..a35e583a4 100644 --- a/app/src/main/res/layout/fragment_playback_panel.xml +++ b/app/src/main/res/layout/fragment_playback_panel.xml @@ -28,15 +28,16 @@ tools:staticIcon="@drawable/ic_song_24" /> - + - - + - + - + diff --git a/app/src/main/res/layout/item_detail.xml b/app/src/main/res/layout/item_detail_header.xml similarity index 100% rename from app/src/main/res/layout/item_detail.xml rename to app/src/main/res/layout/item_detail_header.xml diff --git a/app/src/main/res/layout/item_header.xml b/app/src/main/res/layout/item_header.xml index c623a41ee..4adf76f0c 100644 --- a/app/src/main/res/layout/item_header.xml +++ b/app/src/main/res/layout/item_header.xml @@ -5,4 +5,5 @@ style="@style/Widget.Auxio.TextView.Header" android:layout_width="match_parent" android:layout_height="wrap_content" + android:background="?attr/colorSurface" tools:text="Songs" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_sort_header.xml b/app/src/main/res/layout/item_sort_header.xml index b9b2579bb..7f2deab47 100644 --- a/app/src/main/res/layout/item_sort_header.xml +++ b/app/src/main/res/layout/item_sort_header.xml @@ -4,6 +4,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" + android:background="?attr/colorSurface" android:orientation="horizontal" android:layout_height="wrap_content"> diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml new file mode 100644 index 000000000..8a0023135 --- /dev/null +++ b/app/src/main/res/values-az/strings.xml @@ -0,0 +1,27 @@ + + + Musiqi yüklənir + Musiqi yüklənir + Təkrar cəhd et + İcazə ver + Mahnılar + Bütün mahnılar + Albomlar + Albom + Canlı albom + Remiks albom + Ep-lər + Tək + Tək + Canlı tək + Remiks tək + Kompilyasiya + Kompilyasiyalar + Səs treki + Android üçün asan, səmərəli musiqi oynadıcı. + Musiqi kitabxanası yoxlanılır + Canlı EP + EP + Remiks EP + Səs treklər + \ 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 1f71d6b2e..bc92727aa 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -167,7 +167,7 @@ Невядомы жанр Ogg аўдыё Без даты - Няма нумара кампазіцыі + Няма дарожкі MPEG-1 аўдыё Matroska аўдыё Музыка не грае @@ -222,9 +222,9 @@ Загрузіць музыку Загрузка музыкі ОК - Advanced Audio Coding (AAC) + Прасунуты аўдыё кодэк (AAC) %1$s, %2$s - Free Lossless Audio Codec (FLAC) + Свабодны аўдыё кодэк без страты якасці (FLAC) Уключыць не-музыку Уключыць закругленыя вуглы на дадатковых элементах інтэрфейсу (патрабуецца закругленне вокладак альбомаў) Наладзьце элементы кіравання і паводзіны карыстацкага інтэрфейсу @@ -265,4 +265,10 @@ Зыходныя (хуткая загрузка) Аўдыё Прайграванне + Канцэртны міні-альбом + Міні-альбом рэміксаў + Ігнараваць такія словы, як \"the\", пры сартаванні па імені (лепш за ўсё працуе з англамоўнай музыкай) + Ігнараваць артыклі пры сартаванні + Міні-альбомы + Міні-альбом \ 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 f20dade22..fe32967f3 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -280,4 +280,6 @@ Knihovna Perzistence Sestupně + Při řazení ignorovat předložky + Ignorovat slova jako „the“ při řazení podle názvu (funguje nejlépe u hudby v angličtině) \ 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 248bf84ef..adf3ad6d4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -162,7 +162,7 @@ Musik wird nicht von den von dir hinzugefügten Ordnern geladen. Einschließen Musik wird nur von den von dir hinzugefügten Ordnern geladen. - Keine Titelnummer + Kein Titel Ogg-Audio MPEG-4-Audio MPEG-1-Audio @@ -271,4 +271,6 @@ Persistenz Lautstärkeanpassung ReplayGain Absteigend + Artikel beim Sortieren ignorieren + Wörter wie „the“ ignorieren (funktioniert am besten mit englischsprachiger Musik) \ No newline at end of file diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 1e4d4e407..8a0c55734 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -131,4 +131,5 @@ Σάουντρακ Ζωντανά Φάκελοι μουσικής + Μουσικο κομματι \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 65dad76b7..b51068c59 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -156,7 +156,7 @@ Incluir Audio matroska Free Losless Audio Codec (FLAC) - Advances Audio Coding (AAC) + Advanced Audio Coding (AAC) Cargando tu biblioteca de música… (%1$d/%2$d) Artistas cargados: %d Géneros cargados: %d @@ -275,4 +275,6 @@ Personalizar los controles y el comportamiento de la interfaz de usuario Biblioteca Descendente + Ignorar artículos al ordenar + Ignorar palabras como \"the\" al ordenar por nombre (funciona mejor con música en inglés) \ 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 f39d47f5f..e6737ab82 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -24,9 +24,9 @@ Aller à l\'artiste À propos Version - Voir sur GitHub + Code source Licences - Développé par OxygenCobalt + Développé par Alexandre Capehart Paramètres Apparence @@ -153,4 +153,16 @@ MPEG-4 audio Pas de date Couverture de l\'album pour %s + État effacé + 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 + Etat restauré + Personnaliser les commandes et le comportement de l\'interface utilisateur + Passer au suivant + Mode répétition + Comportement + Action personnalisée de la barre de lecture + Changer le thème et les couleurs de l\'application \ 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 new file mode 100644 index 000000000..89c5485b8 --- /dev/null +++ b/app/src/main/res/values-gl/strings.xml @@ -0,0 +1,258 @@ + + + Carga de música + Cargando música + Álbum + Estatísticas biblioteca + Ver e controlar a reprodución da música + Axustes + Claro + Escuro + Cando se está a reproducir dende a biblioteca + Artista + Un reproductor de música simple e racional para android. + Sinxelo + Compilacións + Compilación + Permitir + Álbums + Sinxelo remix + Volver a intentar + Álbum en directo + Sinxelo + Cancións + Álbum remix + Mestura + En directo + Artistas + Busca + Compilación remix + Pistas de audio + Pista de audio + Sinxelo en directo + Compilación en directo + Mesturas + Ir ao álbum + Xéneros + Xénero + Filtro + Todo + Nome + Duración + Data + Número de cancións + Disco + Pista + Data de engadido + Organizar + En reprodución + Reproducir + Mezcla + Reproducir seguinte + Reproducir a selección + Cola + Engadir á cola + Excluir o que non é música + Ir ao artista + Nome do arquivo + Mesturar + Mesturar todo + Restablecer + Directorio superior + Formato + Tamaño + Tasa de bits + De acordo + Estado restablecido + Cambiar o tema e as cores da aplicación + Cancelar + Estado gardado + Buscando a túa biblioteca… + Automático + Estado limpado + Código fonte + Wiki + Tema + Pantalla + Versión + Esquema de cores + Tema negro + Cargando a túa biblioteca de música… + Utiliza un tema completamente negro + 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 + Manter a mestura ao reproducir unha canción nova + Contido + Modo de repetición + Comportamento + Reproducir dende artista + Lembrar a mestura + Música + 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 + Ver as propiedades + Propiedades da canción + Gardar + Engadir + Licenzas + Engadido á cola + Personalizar + Preferir álbum se un está en reprodución + Dinámico + Verde + -%.1f dB + + %d canción + %d cancións + + Son + Alta calidade + Cartafois de música + Excluír + Este cartafol non está soportado + Reproducir ou pausar + Saltar á seguinte canción + Monitorizando a biblioteca de música + EPs + EP + EP en directo + EP remix + Ascendente + Descendente + Ecualizador + Aleatorio seleccionado + Frecuencia de mostraxe + Acerca de + Monitorizando cambios na túa biblioteca… + Desenvolvido por Alexander Capehart + Aspecto e sensación + Modo redondo + Habilita as esquinas redondeadas en elementos adicionais da interface de usuario (require que as portadas dos álbums estean redondeadas) + Personaliza os controis e o comportamento da interface de usuario + 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 + Controla como se carga a música e as imaxes + Separadores de varios valores + Configura caracteres que denotan múltiples valores da etiqueta + Coma (,) + Punto e coma (;) + Mostrar só artistas que estean directamente acreditados nun álbum (funciona mellos en bibliotecas ben etiquetadas) + Ignorar artigos ao ordenar + Agochar colaboradores + Imaxes + Portadas de álbums + Apagado + Rápido + Configurar o comportamento de son e reprodución + Reprodución + Reprodución automática con auriculares + Rebobinar antes de saltar cara atrás + Rebobinar antes de saltar á canción anterior + Preferir pista + Preferir álbum + Sempre comezar a reprodución cando se conectan uns auriculares (pode non funcionar en todos os dispositivos) + Pausar na repetición + Axuste con etiquetas + Axuste sen etiquetas + Advertencia: O cambio do pre-amp a un valor alto positivo pode resultar en picos en nalgunhas pistas. + A música se cargará dende os cartafois que engadas. + Biblioteca + A música non se cargará dende os cartafois que engadas. + Incluír + Actualizar música + Recargar a biblioteca de música, utilizando as etiquetas na caché cando sexa posible + Volver a escanear a música + Borrar a caché das etiquetas a recargar completamente a biblioteca de música (máis lento, pero máis completo) + Persistencia + Limpar o estado de reprodución + Eliminar o estado de reprodución anterior (se existe) + Imposible restaurar o estado + Imposible borrar o estado + Imposible gardar o estado + Cambiar o modo de repetición + Activar ou desactivar a mezcla + Mezclar todas as cancións + Deter a reprodución + Abrir a cola + Borrar o historial de busca + Quitar cartafol + Icona de Auxio + Portada de álbum + Portada de álbum para %s + Imaxe de artista para %s + Imaxe de xénero para %s + Artista descoñecido + Xénero descoñecido + Sen data + Sen música en reprodución + Audio MPEG-1 + Audio MPEG-4 + Fallou a carga de música + Auxio necesita permiso para leer a túa biblioteca de música + Non se atopou ningunha aplicación que poda facer esta tarefa + Sen cartafois + Audio ogg + Advanced Audio Coding (AAC) + Free Lossless Audio Codec (FLAC) + Vermello + Quitar esta canción da cola + Mover está canción na cola + Mover esta pestana + Rosa + Morado + Morado escuro + Índigo + Azul + Azul escuro + Cian + Verde azulado + Verde escuro + Lima + Amarelo + Laranxa + Marrón + Gris + %1$s, %2$s + %d Seleccionado + Disco %d + +%.1f dB + %d kbps + %d Hz + Cargando a túa biblioteca de música… (%1$d/%2$d) + Cancións cargadas: %d + Álbums cargados: %d + Artistas cargados: %d + Xéneros cargados: %d + Duración total: %s + + %d álbum + %d álbums + + + %d artista + %d artistas + + Máis (+) + Ignorar palabras como \"the\" ao ordenar por nome (funciona mellor con música en inglés) + Modo + Xestionar dende onde se carga a música + Pausar cando se repite unha canción + Cartafois + Gardar o estado de reprodución + Gardar o estado actual de reprodución agora + Restablecer o estado de reprodución gardado previamente (se existe) + Ningunha pista + Saltar á última canción + Restablecer o estado de reprodución + Sen música + Pista %d + Audio Matroska + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index e49d3588f..665e6a720 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -108,7 +108,7 @@ Artista sconosciuto Genere sconosciuto Data sconosciuta - Numero traccia sconosciuto + Nessuna traccia Nessuna canzone riproduzione Rosso @@ -275,4 +275,6 @@ Personalizza controlli e comportamento dell\'UI Configura comportamento di suono e riproduzione Discendente + Ignora gli articoli durante l\'ordinamento + Ignora parole come \"the\" durante l\'ordinamento per nome (funziona meglio con la musica in lingua inglese) \ 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 new file mode 100644 index 000000000..cd948d6b6 --- /dev/null +++ b/app/src/main/res/values-iw/strings.xml @@ -0,0 +1,162 @@ + + + מוזיקה נטענת + מוזיקה נטענת + נסה~י שוב + מפקח על ספריית המוזיקה + כל השירים + אלבומים + אלבום חי + אלבום רמיקסים + אלבומי EP + EP + EP חי + EP רמיקסים + סינגלים + סינגל + סינגל חי + אוסף + אוסף חי + אוספי רמיקסים + פסקולים + פסקול + מיקסטייפים + מיקס + חי + רמיקסים + אומן + אומנים + ז\'אנר + ז\'אנרים + סינון + הכל + תאריך + כמות שירים + דיסק + תאריך הוספה + מיון + עולה + יורד + מנוגן כעת + איקוולייזר + נגנ~י + נגנ~י נבחרים + ערבוב + ערבוב נבחרים + נגנ~י את הבא + הוספ~י לתור + מעבר לאלבום + הצגת מאפיינים + מאפייני שיר + פורמט + גודל + קצב סיביות (ביטרייט) + קצב דגימה (סאמפל רייט) + ערבב~י הכל + אישור + ביטול + שמירה + אתחול + הוספ~י + המצב שנשמר + גרסה + קוד מקור + ויקי + רשיונות + סטטיסטיקות ספרייה + צפייה ושליטה בהשמעת המוזיקה + טוען את ספריית המוזיקה שלך… + משגיח על ספריית המוזיקה שלך כדי לאתר שינויים… + התווסף לרשימה + מפותח על ידי אלכסנדר קייפהארט + חפש~י בספרייה שלך… + מראה ותחושה + שנה~י את ערכת הנושא והצבעים של היישום + ערכת נושא + בהיר + כהה + סכמת צבעים + ערכת נושא שחורה + השתמש~י בערכת נושא שחורה לגמרי + מצב עגול + התאמה אישית + התאמ~י את בקרי והתנהגות הממשק + צג + לשוניות ספרייה + פעולת התראות מותאמת אישית + דלג~י לבא + מצב חזרה + התנהגות + כאשר מנוגן מהספרייה + כאשר מנוגן מפרטי הפריט + נגנ~י מהפריט המוצג + נגנ~י מכל השירים + נגנ~י מאלבום + נגנ~י מהאומן + נגנ~י מז\'אנר + זכור~י ערבוב + שמור~י על ערבוב פועל בעת הפעלת שיר חדש + תוכן + טעינה מחדש אוטומטית + טענ~י את הספריה מחדש בכל פעם שהיא משתנה (דורש התראה קבועה) + התעלמ~י מקבצי אודיו שאינם מוזיקה, כמו פודקאסטים (הסכתים) + מפרידים רבי-ערכים + פסיק (,) + נקודה-פסיק (;) + פלוס (+) + ו- (&) + החבא~י משתפי~ות פעולה + הראה~י רק אומנים שמצויינים ישירות בקרדיטים של אלבום (עובד באופן הטוב ביותר על ספריות מתוייגות היטב) + עטיפות אלבומים + כבוי + מהיר + אודיו + השמעה + ניגון אוטומטי באוזניות + הרצה לאחור לפני דילוג אחורה + הריצ~י לאחור לפני דילוג לשיר הקודם + עצירה בעת חזרה + עוצמת נגינה מחדש + העדפ~י אלבום + מגבר עוצמת נגינה מחדש + התאמה עם תגיות + מיקסטייפ + נגן מוזיקה פשוט והגיוני לאנדרואיד. + אלבום + אוספים + שירים + סינגל רמיקס + מיקסים + חיפוש + אורך + שם + רצועה + תור + מעבר לאומן + שם קובץ + ערבב~י + מצב שוחזר + אודות + הגדרות + אוטומטי + הפעל~י פינות מעוגלות ברכיבי ממשק נוספים (עטיפות אלבומים נדרשות להיות מעוגלות) + שנה~י את הנראות והסדר של לשוניות הספרייה + פעולת סרגל השמעה מותאמת אישית + קבע~י איך מוזיקה ותמונות נטענים + מוזיקה + אי-הכללת תוכן שאינו מוזיקה + התאמ~י תווים המציינים ערכי תגית מרובים + קו נטוי (/) + אזהרה: השימוש בהגדרה זו עלול לגרום לחלק מהתגיות להיות מפורשות באופן שגוי כבעלות מספר ערכים. ניתן לפתור זאת על ידי הכנסת קו נטוי אחורי (\\) לפני תווים מפרידים לא רצויים. + איכות גבוהה + התעלמ~י ממילים כמו \"The\" (\"ה-\") בעת סידור על פי שם (עובד באופן הכי טוב עם מוזיקה בשפה האנגלית) + תמונות + התאמ~י התנהגות צליל והשמעה + התחל~י לנגן תמיד ברגע שמחוברות אוזניות (עלול לא לעבוד בכל המערכות) + עצר~י כאשר שיר חוזר + העדפ~י רצועה + אסטרטגיית עוצמת נגינה מחדש + העדפ~י אלבום אם אחד מופעל + התאמה ללא תגיות + המגבר מוחל על ההתאמה הקיימת בזמן השמעה + \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 14d4ff9d9..1b8dfa355 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1,7 +1,7 @@ 再試行 - シンプルで合理的な音楽プレイヤー。 + Android 用のシンプルで合理的な音楽プレーヤー。 許可する アーティスト ジャンル @@ -34,7 +34,7 @@ 追加 保存 - ライブラリ… を検索 + ライブラリ検索… 暗い 協力者を隠す ライブラリタブ @@ -69,7 +69,7 @@ オーディオ形式 すべてをシャフル OK - 音楽ライブラリ... を読み込み中 + 音楽ライブラリを読み込んでいます… 設定 アプリのテーマと色を変更 個人仕様 @@ -165,10 +165,10 @@ 再生状態を解除 前回保存された再生状態を解除 Matroska オーディオ - Advanced Audio Coding (AAC) - Free Lossless Audio Codec (FLAC) + 高度なオーディオ コーデック (AAC) + 品質を損なうことのない無料のオーディオ コーデック (FLAC) グレイ - トラック番号がありません + トラックなし 全再生時間: %s 最後の曲にスキップ カスタム通知 @@ -182,7 +182,7 @@ プロパティを見る 再生待ち ライブラリ統計 - 音楽ライブラリ ... への変更をモニタリング + 音楽ライブラリの変更を監視しています… 挙動 音楽と画像の読み込み方法をコントロール 画像 @@ -237,4 +237,29 @@ オレンジ 音楽ライブラリ … を読み込み中 (%1$d/%2$d) +%.1f デシベル + 追加の UI 要素で角丸を有効にします (アルバム カバーを丸める必要があります) + 外観 + モード + 複数のタグ値を表す文字を構成する + カスタム再生バー アクション + ソート時に記事を無視する + 名前で並べ替えるときに「the」などの単語を無視する (英語の音楽に最適) + 初期 (高速読み込み) + 再生中の場合はアルバムを優先 + 複数値セパレータ + 親パス + 変更されるたびに音楽ライブラリをリロードします (永続的な通知が必要です) + サウンドと再生の動作を構成する + 戻る前に巻き戻す + 警告: プリアンプを高い正の値に変更すると、一部のオーディオ トラックでピーキングが発生する場合があります。 + 再生状況 + キューを開く + 音楽再生の表示と制御 + ラウドネスイコライゼーション + 警告: この設定を使用すると、一部のタグが複数の値を持つと誤って解釈される場合があります。 これは、不要な区切り文字の前にバックスラッシュ (\\) を付けることで解決できます。 + プリアンプは、再生中に既存の調整に適用されます + タグ キャッシュをクリアし、音楽ライブラリを完全にリロードします (遅くなりますが、より完全になります) + ReplayGain プリアンプ + %1$s、%2$s + UI コントロールと動作をカスタマイズする \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 646275cc8..eeab53168 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -121,7 +121,7 @@ 알 수 없는 아티스트 알 수 없는 장르 날짜 없음 - 트랙 번호 없음 + 트랙 없음 재생 중인 음악 없음 빨간색 @@ -271,4 +271,6 @@ 동작 UI 제어 및 동작 커스텀 내림차순 + 정렬할 때 기사 무시 + 이름으로 정렬할 때 \"the\"와 같은 단어 무시(영어 음악에서 가장 잘 작동함) \ No newline at end of file diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 60b0cd060..6bf884ce2 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -165,7 +165,7 @@ Automatinis krovimas Jokios muzikos nerasta Sustabdyti grojimą - Nėra takelio numerio + Nėra takelio Pereiti prie kitos Automatinis ausinių grojimas Kartojimo režimas @@ -269,4 +269,6 @@ 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 \ No newline at end of file diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 3c5838b20..44c4a5d8c 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -120,4 +120,86 @@ ਕਲਾਕਾਰ ਤੋਂ ਖੇਡੋ ਸ਼ੈਲੀ ਤੋਂ ਖੇਡੋ ਸ਼ਫਲ ਯਾਦ ਰੱਖੋ + ਗੀਤ ਦੁਹਰਾਉਣ ਤੇ ਰੋਕੋ + ਰੀਪਲੇਅ-ਗੇਨ + ਰੀਪਲੇਅ-ਗੇਨ ਰਣਨੀਤੀ + ਟਰੈਕ ਨੂੰ ਤਰਜੀਹ + ਐਲਬਮ ਨੂੰ ਤਰਜੀਹ + ਬਿਨਾਂ ਟੈਗਾਂ ਦੇ ਐਡਜਸਟਮੈਂਟ + ਪ੍ਰਬੰਧਿਤ ਕਰੋ ਕਿ ਸੰਗੀਤ ਕਿੱਥੋਂ ਲੋਡ ਕੀਤਾ ਜਾਣਾ ਚਾਹੀਦਾ ਹੈ + ਫੋਲਡਰ + ਬਾਹਰ ਰੱਖੋ + ਸ਼ਾਮਿਲ ਕਰੋ + ਸੰਗੀਤ ਤਾਜ਼ਾ-ਤਰੀਨ ਕਰੋ + ਪਰਸਿਸਟੈਂਟ + ਪਲੇਬੈਕ ਸਥਿਤੀ ਨੂੰ ਸੁਰੱਖਿਅਤ ਕਰੋ + ਮੌਜੂਦਾ ਪਲੇਬੈਕ ਸਥਿਤੀ ਨੂੰ ਹੁਣੇ ਸੁਰੱਖਿਅਤ ਕਰੋ + ਪਲੇਬੈਕ ਸਥਿਤੀ ਸਾਫ਼ ਕਰੋ + ਪਲੇਬੈਕ ਸਥਿਤੀ ਨੂੰ ਰੀਸਟੋਰ ਕਰੋ + ਪਹਿਲਾਂ ਸੁਰੱਖਿਅਤ ਕੀਤੀ ਪਲੇਬੈਕ ਸਥਿਤੀ ਨੂੰ ਰੀਸਟੋਰ ਕਰੋ (ਜੇ ਕੋਈ ਹੈ) + ਕੋਈ ਐਪ ਨਹੀਂ ਮਿਲੀ ਜੋ ਇਸ ਕਾਰਜ ਨੂੰ ਸੰਭਾਲ ਸਕਦੀ ਹੈ + ਕੋਈ ਫੋਲਡਰ ਨਹੀਂ + ਇਹ ਫੋਲਡਰ ਸਮਰਥਿਤ ਨਹੀਂ ਹੈ + ਸਥਿਤੀ ਨੂੰ ਸੁਰੱਖਿਅਤ ਕਰਨ ਵਿੱਚ ਅਸਮਰੱਥ + ਚਲਾਓ ਜਾਂ ਰੋਕੋ + ਅਗਲੇ ਗੀਤ \'ਤੇ ਜਾਓ + ਆਖਰੀ ਗੀਤ \'ਤੇ ਜਾਓ + ਪਿੱਛੇ ਸਕਿੱਪ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਰੀਵਾਈਂਡ ਕਰੋ + ਜੇਕਰ ਕੋਈ ਇੱਕ ਚੱਲ ਹੋਵੇ ਤਾਂ ਐਲਬਮ ਨੂੰ ਤਰਜੀਹ ਦਿਓ + ਸੰਗੀਤ ਰੀਸਕੈਨ ਕਰੋ + ਜਦੋਂ ਸੰਭਵ ਹੋਵੇ ਤਾਂ ਕੈਸ਼ ਕੀਤੇ ਟੈਗਸ ਦੀ ਵਰਤੋਂ ਕਰਦੇ ਹੋਏ, ਸੰਗੀਤ ਲਾਇਬ੍ਰੇਰੀ ਨੂੰ ਰੀਲੋਡ ਕਰੋ + ਰੀਪਲੇ-ਗੇਨ ਪ੍ਰੀ-ਐਂਪ + ਪ੍ਰੀ-ਐਂਪ ਨੂੰ ਪਲੇਬੈਕ ਦੌਰਾਨ ਮੌਜੂਦਾ ਵਿਵਸਥਾ \'ਤੇ ਲਾਗੂ ਕੀਤਾ ਜਾਂਦਾ ਹੈ + ਟੈਗਸ ਨਾਲ ਐਡਜਸਟਮੈਂਟ + ਚੇਤਾਵਨੀ: ਪ੍ਰੀ-ਐਂਪ ਨੂੰ ਉੱਚ ਸਕਾਰਾਤਮਕ ਮੁੱਲ ਵਿੱਚ ਬਦਲਣ ਦੇ ਨਤੀਜੇ ਵਜੋਂ ਕੁਝ ਆਡੀਓ ਟਰੈਕਾਂ \'ਤੇ ਸਿਖਰ ਹੋ ਸਕਦਾ ਹੈ। + ਤੁਹਾਡੇ ਦੁਆਰਾ ਸ਼ਾਮਲ ਕੀਤੇ ਫੋਲਡਰਾਂ ਤੋਂ ਸੰਗੀਤ ਨੂੰ ਲੋਡ ਕੀਤਾ ਨਹੀਂ ਜਾਵੇਗਾ। + ਤੁਹਾਡੇ ਦੁਆਰਾ ਸ਼ਾਮਲ ਕੀਤੇ ਫੋਲਡਰਾਂ ਤੋਂ ਸੰਗੀਤ ਸਿਰਫ਼ ਲੋਡ ਕੀਤਾ ਜਾਵੇਗਾ। + ਮੋਡ + ਟੈਗ ਕੈਸ਼ ਨੂੰ ਸਾਫ਼ ਕਰੋ ਅਤੇ ਸੰਗੀਤ ਲਾਇਬ੍ਰੇਰੀ ਨੂੰ ਪੂਰੀ ਤਰ੍ਹਾਂ ਰੀਲੋਡ ਕਰੋ (ਹੌਲੀ, ਪਰ ਵਧੇਰੇ ਸੰਪੂਰਨ) + ਪਹਿਲਾਂ ਸੁਰੱਖਿਅਤ ਕੀਤੀ ਪਲੇਬੈਕ ਸਥਿਤੀ ਨੂੰ ਸਾਫ਼ ਕਰੋ (ਜੇ ਕੋਈ ਹੈ) + ਕੋਈ ਸੰਗੀਤ ਨਹੀਂ ਮਿਲਿਆ + ਸੰਗੀਤ ਲੋਡ ਕਰਨਾ ਅਸਫਲ ਰਿਹਾ + Auxio ਨੂੰ ਤੁਹਾਡੀ ਸੰਗੀਤ ਲਾਇਬ੍ਰੇਰੀ ਨੂੰ ਪੜ੍ਹਨ ਲਈ ਇਜਾਜ਼ਤ ਦੀ ਲੋੜ ਹੈ + ਸਥਿਤੀ ਨੂੰ ਰੀਸਟੋਰ ਕਰਨ ਵਿੱਚ ਅਸਮਰੱਥ + ਸਥਿਤੀ ਨੂੰ ਸਾਫ਼ ਕਰਨ ਵਿੱਚ ਅਸਮਰੱਥ + %d ਨੂੰ ਟਰੈਕ ਕਰੋ + ਸਿਰਫ਼ ਉਹਨਾਂ ਕਲਾਕਾਰਾਂ ਨੂੰ ਦਿਖਾਓ ਜੋ ਕਿਸੇ ਐਲਬਮ \'ਤੇ ਸਿੱਧੇ ਤੌਰ \'ਤੇ ਕ੍ਰੈਡਿਟ ਕੀਤੇ ਜਾਂਦੇ ਹਨ (ਚੰਗੀ ਤਰ੍ਹਾਂ ਨਾਲ ਟੈਗ ਕੀਤੀਆਂ ਲਾਇਬ੍ਰੇਰੀਆਂ \'ਤੇ ਵਧੀਆ ਕੰਮ ਕਰਦਾ ਹੈ + ਉਹਨਾਂ ਆਡੀਓ ਫਾਈਲਾਂ ਨੂੰ ਅਣਡਿੱਠ ਕਰੋ ਜੋ ਸੰਗੀਤ ਨਹੀਂ ਹਨ, ਜਿਵੇਂ ਕਿ ਪੌਡਕਾਸਟ + ਪਲੱਸ (+) + ਚੇਤਾਵਨੀ: ਇਸ ਸੈਟਿੰਗ ਦੀ ਵਰਤੋਂ ਕਰਨ ਦੇ ਨਤੀਜੇ ਵਜੋਂ ਕੁਝ ਟੈਗਸ ਨੂੰ ਕਈ ਮੁੱਲਾਂ ਦੇ ਰੂਪ ਵਿੱਚ ਗਲਤ ਤਰੀਕੇ ਨਾਲ ਸਮਝਿਆ ਜਾ ਸਕਦਾ ਹੈ। ਤੁਸੀਂ ਬੈਕਸਲੈਸ਼ (\\) ਦੇ ਨਾਲ ਅਣਚਾਹੇ ਵੱਖ ਕਰਨ ਵਾਲੇ ਅੱਖਰਾਂ ਨੂੰ ਅਗੇਤਰ ਲਗਾ ਕੇ ਇਸਦਾ ਹੱਲ ਕਰ ਸਕਦੇ ਹੋ। + ਹੈੱਡਸੈੱਟ ਕਨੈਕਟ ਹੋਣ \'ਤੇ ਹਮੇਸ਼ਾ ਚਲਾਉਣਾ ਸ਼ੁਰੂ ਕਰੋ (ਹੋ ਸਕਦਾ ਹੈ ਕਿ ਸਾਰੀਆਂ ਡਿਵਾਈਸਾਂ \'ਤੇ ਕੰਮ ਨਾ ਕਰੇ) + ਨਵਾਂ ਗੀਤ ਚਲਾਉਣ ਵੇਲੇ ਸ਼ਫਲ ਚਾਲੂ ਰੱਖੋ + ਸਮੱਗਰੀ + ਸੰਗੀਤ ਅਤੇ ਚਿੱਤਰਾਂ ਨੂੰ ਲੋਡ ਕਰਨ ਦੇ ਤਰੀਕੇ ਨੂੰ ਨਿਯੰਤਰਿਤ ਕਰੋ + ਸੰਗੀਤ + ਆਟੋਮੈਟਿਕ ਮੁੜ ਲੋਡ ਕਰੋ + ਬਦਲਣ ਤੇ ਸੰਗੀਤ ਲਾਇਬ੍ਰੇਰੀ ਨੂੰ ਰੀਲੋਡ ਕਰੋ (ਸਥਾਈ ਨੋਟੀਫਿਕੇਸ਼ਨ ਦੀ ਲੋੜ ਹੁੰਦੀ ਹੈ) + ਗੈਰ-ਸੰਗੀਤ ਨੂੰ ਬਾਹਰ ਰੱਖੋ + ਬਹੁ-ਮੁੱਲ ਵਿਭਾਜਕ + ਉਹਨਾਂ ਅੱਖਰਾਂ ਦੀ ਸੰਰਚਨਾ ਕਰੋ ਜੋ ਕਈ ਟੈਗ ਮੁੱਲਾਂ ਨੂੰ ਦਰਸਾਉਂਦੇ ਹਨ + ਕੌਮਾ (,) + ਸੈਮੀਕੋਲਨ (;) + ਸਲੈਸ਼ (/) + ਐਂਪਰਸੈਂਡ (&) + ਸਹਿਯੋਗੀਆਂ ਨੂੰ ਲੁਕਾਓ + ਆਵਾਜ਼ ਅਤੇ ਪਲੇਬੈਕ ਵਿਵਹਾਰ ਦੀ ਸੰਰਚਨਾ ਕਰੋ + ਪਲੇਅਬੈਕ + ਹੈੱਡਸੈੱਟ ਆਟੋਪਲੇ + ਚਿੱਤਰ + ਐਲਬਮ ਕਵਰ + ਬੰਦ + ਤੇਜ + ਉੱਚ ਕੁਆਲਿਟੀ + ਆਡੀਓ + ਪਿਛਲੇ ਗੀਤ \'ਤੇ ਜਾਣ ਤੋਂ ਪਹਿਲਾਂ ਰੀਵਾਈਂਡ ਕਰੋ + ਦੁਹਰਾਉਣ \'ਤੇ ਰੁਕੋ + ਲਾਇਬ੍ਰੇਰੀ + ਸੰਗੀਤ ਫੋਲਡਰ + ਕਤਾਰ ਖੋਲ੍ਹੋ + ਇਸ ਕਤਾਰ ਗੀਤ ਨੂੰ ਹਟਾਓ + ਇਸ ਕਤਾਰ ਗੀਤ ਨੂੰ ਮੂਵ ਕਰੋ + ਦੁਹਰਾਓ ਮੋਡ ਬਦਲੋ + ਸ਼ਫਲ ਚਾਲੂ ਜਾਂ ਬੰਦ ਕਰੋ + ਸਾਰੇ ਗੀਤਾਂ ਨੂੰ ਸ਼ਫਲ ਕਰੋ + ਪਲੇਬੈਕ ਬੰਦ ਕਰੋ \ 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 d7e05b60c..dd48e1d43 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -276,4 +276,6 @@ Nie można wyczyścić stanu Nie można zapisać stanu odtwarzania Malejąco + Ignoruj rodzajniki podczas sortowania + Ignoruj słowa takie jak „the” podczas sortowania według tytułu (działa najlepiej z tytułami w języku angielskim) \ 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 0b490cf09..39f7febb4 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -136,7 +136,7 @@ Índigo Gênero desconhecido Sem data - Sem número de faixa + Nenhuma faixa Nenhuma música tocando Imagem do artista para %s Imagem de gênero para %s @@ -273,4 +273,6 @@ Comportamento Pastas 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 2486bd6cb..3e7f7a265 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -63,17 +63,19 @@ Músicas carregado: %d %d Música + %d Músicas %d Músicas %d Álbum + %d Álbuns %d Álbuns Reproduzir a partir do item mostrado Reproduzir do álbum Imagem do artista para %s Gênero desconhecido - Sem número de faixa + Nenhuma faixa Áudio MPEG-4 Artistas carregados: %d Duração total: %s @@ -197,7 +199,7 @@ Áudio Matroska Codificação de Audio Avançada (AAC) Álbum - EPs + Miniálbuns EP Trilhas sonoras Trilha sonora @@ -271,4 +273,6 @@ Estado de reprodução E comercial (&) Comportamento + 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-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 5a6036e96..bfc33bc2f 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -45,7 +45,7 @@ Настройки Внешний вид Тема - Автоматически + Системная Светлая Тёмная Цветовой акцент @@ -278,4 +278,6 @@ Папки Состояние воспроизведения По убыванию + Игнорировать артикли при сортировке + Игнорировать такие слова, как «the», при сортировке по имени (лучше всего работает с англоязычной музыкой) \ No newline at end of file diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/app/src/main/res/values-sr/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index e10f88cc4..a05a0adfd 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -99,7 +99,7 @@ Müzik yalnızca eklediğiniz klasörlerden yüklenecektir. %s Albümünün kapağı %s Sanatçısının resmi - Parça numarası yok + Parça yok Matroska Ses İleri Düzeyde Ses Kodlama (AAC) Bağımsız Kayıpsız Ses Kodlama (FLAC) @@ -269,4 +269,6 @@ Arayüz kontrollerini ve davranışını özelleştirin Davranış Ses yüksekliği dengesi ReplayGain + Sıralama yaparken makaleleri yoksay + Ada göre sıralarken \"the\" gibi kelimeleri yok sayın (en iyi ingilizce müzikle çalışır) \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index d2ffd08ee..f15b96f58 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -275,4 +275,6 @@ Налаштування звуку і поведінки при відтворенні Папки За спаданням + Ігнорувати артиклі під час сортування + Ігнорування таких слів, як \"the\", під час сортування за назвою (найкраще працює з англомовною музикою) \ 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 dbdd101da..faa26ddf0 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -107,7 +107,7 @@ 未知艺术家 未知流派 没有日期 - 无曲目编号 + 无曲目 未播放音乐 红色 @@ -269,4 +269,6 @@ 音乐 配置声音和播放行为 降序 + 排序时忽略冠词 + 按名称排序时忽略类似“the”这样的冠词(对英文歌曲的效果最好) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 41d015a28..db7b36a48 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -212,8 +212,8 @@ Plus (+) Ampersand (&) Warning: Using this setting may result in some tags being incorrectly interpreted as having multiple values. You can resolve this by prefixing unwanted separator characters with a backslash (\\). - Ignore articles when sorting - Ignore words like \"the\" when sorting by name (works best with english-language music) + Intelligent sorting + Correctly sort names that begin with numbers or words like \"the\" (works best with english-language music) Hide collaborators Only show artists that are directly credited on an album (works best on well-tagged libraries) Images diff --git a/app/src/main/res/values/styles_core.xml b/app/src/main/res/values/styles_core.xml index 094cdb665..bbe18c90c 100644 --- a/app/src/main/res/values/styles_core.xml +++ b/app/src/main/res/values/styles_core.xml @@ -33,7 +33,7 @@ @style/TextAppearance.Auxio.HeadlineSmall @style/TextAppearance.Auxio.TitleLarge - @style/TextAppearance.Auxio.TitleMediumLowEmphasis + @style/TextAppearance.Auxio.TitleMedium @style/TextAppearance.Auxio.TitleSmall diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml index d6f9e5d21..6e0ee0001 100644 --- a/app/src/main/res/values/styles_ui.xml +++ b/app/src/main/res/values/styles_ui.xml @@ -113,7 +113,7 @@ diff --git a/app/src/main/res/values/typography.xml b/app/src/main/res/values/typography.xml index db22ed074..af958330b 100644 --- a/app/src/main/res/values/typography.xml +++ b/app/src/main/res/values/typography.xml @@ -72,13 +72,6 @@ - -