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.
-
-
+
+
-
+
-
+
-
+
@@ -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