diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2aaa3c6a4..f2afad7ce 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,7 +9,7 @@ > { override suspend fun fetch( @@ -30,8 +32,7 @@ class MosaicFetcher(private val context: Context) : Fetcher> { ): FetchResult { val streams = mutableListOf() - // Load the streams, the lower-quality MediaStore covers are used simply because using - // the raw ones would make loading far too long. Its not that noticeable either. + // Load MediaStore streams data.forEach { val stream: InputStream? = context.contentResolver.openInputStream(it) @@ -69,7 +70,9 @@ class MosaicFetcher(private val context: Context) : Fetcher> { // For each stream, create a bitmap scaled to 1/4th of the mosaics combined size // and place it on a corner of the canvas. - for (stream in streams) { + streams.useForEach { stream -> + if (y == MOSAIC_BITMAP_SIZE) return@useForEach + val bitmap = Bitmap.createScaledBitmap( BitmapFactory.decodeStream(stream), MOSAIC_BITMAP_INCREMENT, @@ -84,16 +87,9 @@ class MosaicFetcher(private val context: Context) : Fetcher> { if (x == MOSAIC_BITMAP_SIZE) { x = 0 y += MOSAIC_BITMAP_INCREMENT - - if (y == MOSAIC_BITMAP_SIZE) { - break - } } } - // Close all the streams when done. - streams.forEach { it.close() } - return DrawableResult( drawable = finalBitmap.toDrawable(context.resources), isSampled = false, @@ -101,6 +97,16 @@ class MosaicFetcher(private val context: Context) : Fetcher> { ) } + /** + * Iterate through a list of [Closeable]s, running [use] on each. + * @param action What to do for each [Closeable] + */ + private fun List.useForEach(action: (T) -> R) { + forEach { + it.use(action) + } + } + override fun key(data: List): String = data.toString() companion object { diff --git a/app/src/main/java/org/oxycblt/auxio/database/PlaybackStateDatabase.kt b/app/src/main/java/org/oxycblt/auxio/database/PlaybackStateDatabase.kt index eb96510ee..d1f375fc8 100644 --- a/app/src/main/java/org/oxycblt/auxio/database/PlaybackStateDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/database/PlaybackStateDatabase.kt @@ -55,14 +55,14 @@ class PlaybackStateDatabase(context: Context) : */ private fun constructStateTable(command: StringBuilder): StringBuilder { command.append("${PlaybackState.COLUMN_ID} LONG PRIMARY KEY,") - command.append("${PlaybackState.COLUMN_SONG_NAME} STRING NOT NULL,") - command.append("${PlaybackState.COLUMN_POSITION} LONG NOT NULL,") - command.append("${PlaybackState.COLUMN_PARENT_NAME} STRING NOT NULL,") - command.append("${PlaybackState.COLUMN_INDEX} INTEGER NOT NULL,") - command.append("${PlaybackState.COLUMN_MODE} INTEGER NOT NULL,") - command.append("${PlaybackState.COLUMN_IS_SHUFFLING} BOOLEAN NOT NULL,") - command.append("${PlaybackState.COLUMN_LOOP_MODE} INTEGER NOT NULL,") - command.append("${PlaybackState.COLUMN_IN_USER_QUEUE} BOOLEAN NOT NULL)") + .append("${PlaybackState.COLUMN_SONG_NAME} STRING NOT NULL,") + .append("${PlaybackState.COLUMN_POSITION} LONG NOT NULL,") + .append("${PlaybackState.COLUMN_PARENT_NAME} STRING NOT NULL,") + .append("${PlaybackState.COLUMN_INDEX} INTEGER NOT NULL,") + .append("${PlaybackState.COLUMN_MODE} INTEGER NOT NULL,") + .append("${PlaybackState.COLUMN_IS_SHUFFLING} BOOLEAN NOT NULL,") + .append("${PlaybackState.COLUMN_LOOP_MODE} INTEGER NOT NULL,") + .append("${PlaybackState.COLUMN_IN_USER_QUEUE} BOOLEAN NOT NULL)") return command } @@ -72,9 +72,9 @@ class PlaybackStateDatabase(context: Context) : */ private fun constructQueueTable(command: StringBuilder): StringBuilder { command.append("${QueueItem.COLUMN_ID} LONG PRIMARY KEY,") - command.append("${QueueItem.COLUMN_SONG_NAME} LONG NOT NULL,") - command.append("${QueueItem.COLUMN_ALBUM_NAME} LONG NOT NULL,") - command.append("${QueueItem.COLUMN_IS_USER_QUEUE} BOOLEAN NOT NULL)") + .append("${QueueItem.COLUMN_SONG_NAME} LONG NOT NULL,") + .append("${QueueItem.COLUMN_ALBUM_NAME} LONG NOT NULL,") + .append("${QueueItem.COLUMN_IS_USER_QUEUE} BOOLEAN NOT NULL)") return command } 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 f0688a10d..13618d9d8 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -76,6 +76,7 @@ class AlbumDetailFragment : DetailFragment() { detailModel.albumSortMode.observe(viewLifecycleOwner) { mode -> logD("Updating sort mode to $mode") + // Detail header data is included val data = mutableListOf(detailModel.currentAlbum.value!!).also { it.addAll(mode.getSortedSongList(detailModel.currentAlbum.value!!.songs)) } @@ -85,8 +86,9 @@ class AlbumDetailFragment : DetailFragment() { detailModel.navToItem.observe(viewLifecycleOwner) { if (it != null) { - logD(it.name) when (it) { + // Songs should be scrolled to if the album matches, or a new detail + // fragment should be launched otherwise. is Song -> { if (detailModel.currentAlbum.value!!.id == it.album.id) { scrollToItem(it.id) @@ -99,6 +101,8 @@ class AlbumDetailFragment : DetailFragment() { } } + // If the album matches, no need to do anything. Otherwise launch a new + // detail fragment. is Album -> { if (detailModel.currentAlbum.value!!.id == it.id) { binding.detailRecycler.scrollToPosition(0) @@ -110,8 +114,8 @@ class AlbumDetailFragment : DetailFragment() { } } + // Always launch a new ArtistDetailFragment. is Artist -> { - logD("Hello?") findNavController().navigate( AlbumDetailFragmentDirections.actionShowArtist(it.id) ) @@ -125,7 +129,14 @@ class AlbumDetailFragment : DetailFragment() { // --- PLAYBACKVIEWMODEL SETUP --- playbackModel.song.observe(viewLifecycleOwner) { - handlePlayingItem(detailAdapter) + if (playbackModel.mode.value == PlaybackMode.IN_ALBUM && + playbackModel.parent.value?.id == detailModel.currentAlbum.value!!.id + ) { + detailAdapter.highlightSong(playbackModel.song.value, binding.detailRecycler) + } else { + // Clear the viewholders if the mode isn't ALL_SONGS + detailAdapter.highlightSong(null, binding.detailRecycler) + } } playbackModel.isInUserQueue.observe(viewLifecycleOwner) { @@ -139,21 +150,6 @@ class AlbumDetailFragment : DetailFragment() { return binding.root } - /** - * Handle an update to the mode or the song and determine whether to highlight a song - * item based off that - */ - private fun handlePlayingItem(detailAdapter: AlbumDetailAdapter) { - if (playbackModel.mode.value == PlaybackMode.IN_ALBUM && - playbackModel.parent.value?.id == detailModel.currentAlbum.value!!.id - ) { - detailAdapter.highlightSong(playbackModel.song.value, binding.detailRecycler) - } else { - // Clear the viewholders if the mode isn't ALL_SONGS - detailAdapter.highlightSong(null, binding.detailRecycler) - } - } - private fun scrollToItem(id: Long) { // Calculate where the item for the currently played song is val pos = detailModel.albumSortMode.value!!.getSortedSongList( 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 8f927b42b..2911b58f0 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -11,6 +11,7 @@ import org.oxycblt.auxio.logD import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.BaseModel +import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackMode @@ -70,6 +71,7 @@ class ArtistDetailFragment : DetailFragment() { detailModel.artistSortMode.observe(viewLifecycleOwner) { mode -> logD("Updating sort mode to $mode") + // Header detail data is always included val data = mutableListOf(detailModel.currentArtist.value!!).also { it.addAll(mode.getSortedAlbumList(detailModel.currentArtist.value!!.albums)) } @@ -79,15 +81,18 @@ class ArtistDetailFragment : DetailFragment() { detailModel.navToItem.observe(viewLifecycleOwner) { if (it != null) { + // If the artist matches, no need to do anything, otherwise launch a new detail if (it is Artist) { if (it.id == detailModel.currentArtist.value!!.id) { + binding.detailRecycler.scrollToPosition(0) detailModel.doneWithNavToItem() } else { findNavController().navigate( ArtistDetailFragmentDirections.actionShowArtist(it.id) ) } - } else { + } else if (it !is Genre) { + // Determine the album id of the song or album, and then launch it otherwise val albumId = if (it is Song) it.album.id else it.id findNavController().navigate( @@ -97,6 +102,7 @@ class ArtistDetailFragment : DetailFragment() { } } + // Highlight albums if they are being played playbackModel.parent.observe(viewLifecycleOwner) { parent -> if (playbackModel.mode.value == PlaybackMode.IN_ALBUM && parent is Album?) { detailAdapter.setCurrentAlbum(parent, binding.detailRecycler) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt index d51d0caf9..408b4a261 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -83,6 +83,7 @@ abstract class DetailFragment : Fragment() { adapter = detailAdapter setHasFixedSize(true) + // Set up a grid if the mode is landscape if (isLandscape(resources)) { layoutManager = GridLayoutManager(requireContext(), 2).also { it.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { @@ -93,6 +94,8 @@ abstract class DetailFragment : Fragment() { } } + // Since there is no elevation when the scroll position is zero, dont show + // the overscroll indicator. addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { overScrollMode = if (computeVerticalScrollOffset() == 0) { 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 bf0c17bbf..322edefe9 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -63,6 +63,7 @@ class GenreDetailFragment : DetailFragment() { detailModel.genreSortMode.observe(viewLifecycleOwner) { mode -> logD("Updating sort mode to $mode") + // Detail header data is included val data = mutableListOf(detailModel.currentGenre.value!!).also { it.addAll(mode.getSortedSongList(detailModel.currentGenre.value!!.songs)) } @@ -73,6 +74,7 @@ class GenreDetailFragment : DetailFragment() { detailModel.navToItem.observe(viewLifecycleOwner) { if (it != null) { when (it) { + // All items will launch new detail fragments. is Artist -> findNavController().navigate( GenreDetailFragmentDirections.actionShowArtist(it.id) ) @@ -93,7 +95,14 @@ class GenreDetailFragment : DetailFragment() { // --- PLAYBACKVIEWMODEL SETUP --- playbackModel.song.observe(viewLifecycleOwner) { - handlePlayingItem(detailAdapter) + if (playbackModel.mode.value == PlaybackMode.IN_GENRE && + playbackModel.parent.value?.id == detailModel.currentGenre.value!!.id + ) { + detailAdapter.highlightSong(playbackModel.song.value, binding.detailRecycler) + } else { + // Clear the viewholders if the mode isn't ALL_SONGS + detailAdapter.highlightSong(null, binding.detailRecycler) + } } playbackModel.isInUserQueue.observe(viewLifecycleOwner) { @@ -106,19 +115,4 @@ class GenreDetailFragment : DetailFragment() { return binding.root } - - /** - * Handle an update to the mode or the song and determine whether to highlight a song - * item based off that - */ - private fun handlePlayingItem(detailAdapter: GenreDetailAdapter) { - if (playbackModel.mode.value == PlaybackMode.IN_GENRE && - playbackModel.parent.value?.id == detailModel.currentGenre.value!!.id - ) { - detailAdapter.highlightSong(playbackModel.song.value, binding.detailRecycler) - } else { - // Clear the viewholders if the mode isn't ALL_SONGS - detailAdapter.highlightSong(null, binding.detailRecycler) - } - } } diff --git a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt index 20d4f5bc3..37f047a1d 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt @@ -101,11 +101,11 @@ class LibraryFragment : Fragment() { /** * Navigate to an item - * @param baseModel The data things should be done with + * @param baseModel The item that should be navigated to. */ private fun onItemSelection(baseModel: BaseModel) { if (baseModel is Song) { - logE("onItemSelection does not support song") + logE("onItemSelection does not support songs") return } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt index 46a002a4a..6c108c85c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt @@ -46,7 +46,6 @@ class CompactPlaybackFragment : Fragment() { // Put a placeholder song in the binding & hide the playback fragment initially. binding.song = MusicStore.getInstance().songs[0] binding.playbackModel = playbackModel - if (playbackModel.song.value == null && isLandscape) { hideAll(binding) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt index c8a08ffd9..e18469e7a 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt @@ -30,8 +30,8 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { private val playbackModel: PlaybackViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels() private val binding: FragmentPlaybackBinding by memberBinding(FragmentPlaybackBinding::inflate) { - // Marquee must be disabled on destroy to prevent memory leaks - binding.playbackSong.isSelected = false + // Marquee must be disabled on destruction to prevent memory leaks + playbackSong.isSelected = false } // Colors diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt index ba6171cc2..14ca17aef 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt @@ -56,7 +56,7 @@ class SearchAdapter( HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context) - else -> error("Someone messed with the ViewHolder item types.") + else -> error("Invalid viewholder item type.") } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt index 0fc21767f..682183de0 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt @@ -63,9 +63,9 @@ class SettingsListFragment : PreferenceFragmentCompat() { } } - private fun handlePreference(it: Preference) { - it.apply { - when (it.key) { + private fun handlePreference(pref: Preference) { + pref.apply { + when (key) { SettingsManager.Keys.KEY_THEME -> { setIcon(AppCompatDelegate.getDefaultNightMode().toThemeIcon()) diff --git a/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt b/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt index c326d7ae5..6fe9badf7 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt @@ -94,7 +94,7 @@ fun Fragment.requireCompatActivity(): AppCompatActivity { } /** - * "Render" a [Spanned] using [HtmlCompat]. + * "Render" a [Spanned] using [HtmlCompat]. (As in making text bolded and whatnot). * @return A [Spanned] that actually works. */ fun Spanned.render(): Spanned { diff --git a/app/src/main/res/layout-land/fragment_compact_playback.xml b/app/src/main/res/layout-land/fragment_compact_playback.xml index def69e666..6989d0c1b 100644 --- a/app/src/main/res/layout-land/fragment_compact_playback.xml +++ b/app/src/main/res/layout-land/fragment_compact_playback.xml @@ -44,9 +44,8 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/margin_mid_small" android:layout_marginEnd="@dimen/margin_mid_small" - android:ellipsize="marquee" android:fontFamily="@font/inter_semibold" - android:marqueeRepeatLimit="marquee_forever" + android:ellipsize="end" android:singleLine="true" android:text="@{song.name}" android:textAppearance="@style/TextAppearance.SmallHeader" @@ -63,8 +62,7 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/margin_mid_small" android:layout_marginEnd="@dimen/margin_mid_small" - android:ellipsize="marquee" - android:marqueeRepeatLimit="marquee_forever" + android:ellipsize="end" android:singleLine="true" android:text="@{@string/format_info(song.album.artist.name, song.album.name)}" android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" diff --git a/app/src/main/res/layout/fragment_compact_playback.xml b/app/src/main/res/layout/fragment_compact_playback.xml index db69a21e7..0fbee7dc8 100644 --- a/app/src/main/res/layout/fragment_compact_playback.xml +++ b/app/src/main/res/layout/fragment_compact_playback.xml @@ -53,9 +53,8 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/margin_mid_small" android:layout_marginEnd="@dimen/margin_mid_small" - android:ellipsize="marquee" + android:ellipsize="end" android:fontFamily="@font/inter_semibold" - android:marqueeRepeatLimit="marquee_forever" android:singleLine="true" android:text="@{song.name}" android:textAppearance="@style/TextAppearance.SmallHeader" @@ -72,8 +71,7 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/margin_mid_small" android:layout_marginEnd="@dimen/margin_mid_small" - android:ellipsize="marquee" - android:marqueeRepeatLimit="marquee_forever" + android:ellipsize="end" android:singleLine="true" android:text="@{@string/format_info(song.album.artist.name, song.album.name)}" android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" diff --git a/app/src/main/res/values/do_not_translate.xml b/app/src/main/res/values/do_not_translate.xml index 2bdd848ee..969e537ba 100644 --- a/app/src/main/res/values/do_not_translate.xml +++ b/app/src/main/res/values/do_not_translate.xml @@ -1,10 +1,10 @@ - OxygenCobalt + OxygenCobalt - %1$s / %2$s - %1$s / %2$s / %3$s - %1$s, %2$s - <b>%1$s</b>: %2$s + %1$s / %2$s + %1$s / %2$s / %3$s + %1$s, %2$s + <b>%1$s</b>: %2$s \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2d9f97786..af9900758 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -93,7 +93,7 @@ When a playlist ends Loop Stop - Loop & Pause + Loop and Pause Remember shuffle Keep shuffle on when playing a new song @@ -108,10 +108,10 @@ State saved - No music found. - Music loading failed. - Permissions to read storage are needed. - Could not open link. + No music found + Music loading failed + Permissions to read storage are needed + Could not open link Search your library…