extends CoordinatorLayout.Be
};
}
}
+
diff --git a/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java b/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java
new file mode 100644
index 000000000..066429513
--- /dev/null
+++ b/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java
@@ -0,0 +1,412 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.material.divider;
+
+import com.google.android.material.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.ItemDecoration;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+import androidx.annotation.ColorInt;
+import androidx.annotation.ColorRes;
+import androidx.annotation.DimenRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.Px;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.core.view.ViewCompat;
+import com.google.android.material.internal.ThemeEnforcement;
+import com.google.android.material.resources.MaterialResources;
+
+/**
+ * MaterialDividerItemDecoration is a {@link RecyclerView.ItemDecoration}, similar to a {@link
+ * androidx.recyclerview.widget.DividerItemDecoration}, that can be used as a divider between items of
+ * a {@link LinearLayoutManager}. It supports both {@link #HORIZONTAL} and {@link #VERTICAL}
+ * orientations.
+ *
+ *
+ * dividerItemDecoration = new MaterialDividerItemDecoration(recyclerView.getContext(),
+ * layoutManager.getOrientation());
+ * recyclerView.addItemDecoration(dividerItemDecoration);
+ *
+ *
+ * Modified at several points by Alexander Capehart to backport miscellaneous fixes not currently
+ * obtainable in the currently used MDC library.
+ */
+public class BackportMaterialDividerItemDecoration extends ItemDecoration {
+ public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
+ public static final int VERTICAL = LinearLayout.VERTICAL;
+
+ private static final int DEF_STYLE_RES = R.style.Widget_MaterialComponents_MaterialDivider;
+
+ @NonNull private Drawable dividerDrawable;
+ private int thickness;
+ @ColorInt private int color;
+ private int orientation;
+ private int insetStart;
+ private int insetEnd;
+ private boolean lastItemDecorated;
+
+ private final Rect tempRect = new Rect();
+
+ public BackportMaterialDividerItemDecoration(@NonNull Context context, int orientation) {
+ this(context, null, orientation);
+ }
+
+ public BackportMaterialDividerItemDecoration(
+ @NonNull Context context, @Nullable AttributeSet attrs, int orientation) {
+ this(context, attrs, R.attr.materialDividerStyle, orientation);
+ }
+
+ public BackportMaterialDividerItemDecoration(
+ @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int orientation) {
+ TypedArray attributes =
+ ThemeEnforcement.obtainStyledAttributes(
+ context, attrs, R.styleable.MaterialDivider, defStyleAttr, DEF_STYLE_RES);
+
+ color =
+ MaterialResources.getColorStateList(
+ context, attributes, R.styleable.MaterialDivider_dividerColor)
+ .getDefaultColor();
+ thickness =
+ attributes.getDimensionPixelSize(
+ R.styleable.MaterialDivider_dividerThickness,
+ context.getResources().getDimensionPixelSize(R.dimen.material_divider_thickness));
+ insetStart =
+ attributes.getDimensionPixelOffset(R.styleable.MaterialDivider_dividerInsetStart, 0);
+ insetEnd = attributes.getDimensionPixelOffset(R.styleable.MaterialDivider_dividerInsetEnd, 0);
+ lastItemDecorated =
+ attributes.getBoolean(R.styleable.MaterialDivider_lastItemDecorated, true);
+
+ attributes.recycle();
+
+ dividerDrawable = new ShapeDrawable();
+ setDividerColor(color);
+ setOrientation(orientation);
+ }
+
+ /**
+ * Sets the orientation for this divider. This should be called if {@link
+ * RecyclerView.LayoutManager} changes orientation.
+ *
+ * A {@link #HORIZONTAL} orientation will draw a vertical divider, and a {@link #VERTICAL}
+ * orientation a horizontal divider.
+ *
+ * @param orientation The orientation of the {@link RecyclerView} this divider is associated with:
+ * {@link #HORIZONTAL} or {@link #VERTICAL}
+ */
+ public void setOrientation(int orientation) {
+ if (orientation != HORIZONTAL && orientation != VERTICAL) {
+ throw new IllegalArgumentException(
+ "Invalid orientation: " + orientation + ". It should be either HORIZONTAL or VERTICAL");
+ }
+ this.orientation = orientation;
+ }
+
+ public int getOrientation() {
+ return orientation;
+ }
+
+ /**
+ * Sets the thickness of the divider.
+ *
+ * @param thickness The thickness value to be set.
+ * @see #getDividerThickness()
+ * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerThickness
+ */
+ public void setDividerThickness(@Px int thickness) {
+ this.thickness = thickness;
+ }
+
+ /**
+ * Sets the thickness of the divider.
+ *
+ * @param thicknessId The id of the thickness dimension resource to be set.
+ * @see #getDividerThickness()
+ * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerThickness
+ */
+ public void setDividerThicknessResource(@NonNull Context context, @DimenRes int thicknessId) {
+ setDividerThickness(context.getResources().getDimensionPixelSize(thicknessId));
+ }
+
+ /**
+ * Returns the thickness set on the divider.
+ *
+ * @see #setDividerThickness(int)
+ * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerThickness
+ */
+ @Px
+ public int getDividerThickness() {
+ return thickness;
+ }
+
+ /**
+ * Sets the color of the divider.
+ *
+ * @param color The color to be set.
+ * @see #getDividerColor()
+ * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerColor
+ */
+ public void setDividerColor(@ColorInt int color) {
+ this.color = color;
+ dividerDrawable = DrawableCompat.wrap(dividerDrawable);
+ DrawableCompat.setTint(dividerDrawable, color);
+ }
+
+ /**
+ * Sets the color of the divider.
+ *
+ * @param colorId The id of the color resource to be set.
+ * @see #getDividerColor()
+ * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerColor
+ */
+ public void setDividerColorResource(@NonNull Context context, @ColorRes int colorId) {
+ setDividerColor(ContextCompat.getColor(context, colorId));
+ }
+
+ /**
+ * Returns the divider color.
+ *
+ * @see #setDividerColor(int)
+ * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerColor
+ */
+ @ColorInt
+ public int getDividerColor() {
+ return color;
+ }
+
+ /**
+ * Sets the start inset of the divider.
+ *
+ * @param insetStart The start inset to be set.
+ * @see #getDividerInsetStart()
+ * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetStart
+ */
+ public void setDividerInsetStart(@Px int insetStart) {
+ this.insetStart = insetStart;
+ }
+
+ /**
+ * Sets the start inset of the divider.
+ *
+ * @param insetStartId The id of the inset dimension resource to be set.
+ * @see #getDividerInsetStart()
+ * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetStart
+ */
+ public void setDividerInsetStartResource(@NonNull Context context, @DimenRes int insetStartId) {
+ setDividerInsetStart(context.getResources().getDimensionPixelOffset(insetStartId));
+ }
+
+ /**
+ * Returns the divider's start inset.
+ *
+ * @see #setDividerInsetStart(int)
+ * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetStart
+ */
+ @Px
+ public int getDividerInsetStart() {
+ return insetStart;
+ }
+
+ /**
+ * Sets the end inset of the divider.
+ *
+ * @param insetEnd The end inset to be set.
+ * @see #getDividerInsetEnd()
+ * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetEnd
+ */
+ public void setDividerInsetEnd(@Px int insetEnd) {
+ this.insetEnd = insetEnd;
+ }
+
+ /**
+ * Sets the end inset of the divider.
+ *
+ * @param insetEndId The id of the inset dimension resource to be set.
+ * @see #getDividerInsetEnd()
+ * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetEnd
+ */
+ public void setDividerInsetEndResource(@NonNull Context context, @DimenRes int insetEndId) {
+ setDividerInsetEnd(context.getResources().getDimensionPixelOffset(insetEndId));
+ }
+
+ /**
+ * Returns the divider's end inset.
+ *
+ * @see #setDividerInsetEnd(int)
+ * @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetEnd
+ */
+ @Px
+ public int getDividerInsetEnd() {
+ return insetEnd;
+ }
+
+ /**
+ * Sets whether the class should draw a divider after the last item of a {@link RecyclerView}.
+ *
+ * @param lastItemDecorated whether there's a divider after the last item of a recycler view.
+ * @see #isLastItemDecorated()
+ * @attr ref com.google.android.material.R.styleable#MaterialDivider_lastItemDecorated
+ */
+ public void setLastItemDecorated(boolean lastItemDecorated) {
+ this.lastItemDecorated = lastItemDecorated;
+ }
+
+ /**
+ * Whether there's a divider after the last item of a {@link RecyclerView}.
+ *
+ * @see #setLastItemDecorated(boolean)
+ * @attr ref com.google.android.material.R.styleable#MaterialDivider_shouldDecorateLastItem
+ */
+ public boolean isLastItemDecorated() {
+ return lastItemDecorated;
+ }
+
+ @Override
+ public void onDraw(
+ @NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+ if (parent.getLayoutManager() == null) {
+ return;
+ }
+ if (orientation == VERTICAL) {
+ drawForVerticalOrientation(canvas, parent);
+ } else {
+ drawForHorizontalOrientation(canvas, parent);
+ }
+ }
+
+ /**
+ * Draws a divider for the vertical orientation of the recycler view. The divider itself will be
+ * horizontal.
+ */
+ private void drawForVerticalOrientation(@NonNull Canvas canvas, @NonNull RecyclerView parent) {
+ canvas.save();
+ int left;
+ int right;
+ if (parent.getClipToPadding()) {
+ left = parent.getPaddingLeft();
+ right = parent.getWidth() - parent.getPaddingRight();
+ canvas.clipRect(
+ left, parent.getPaddingTop(), right, parent.getHeight() - parent.getPaddingBottom());
+ } else {
+ left = 0;
+ right = parent.getWidth();
+ }
+ boolean isRtl = ViewCompat.getLayoutDirection(parent) == ViewCompat.LAYOUT_DIRECTION_RTL;
+ left += isRtl ? insetEnd : insetStart;
+ right -= isRtl ? insetStart : insetEnd;
+
+ int childCount = parent.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = parent.getChildAt(i);
+ if (shouldDrawDivider(parent, child)) {
+ parent.getDecoratedBoundsWithMargins(child, tempRect);
+ // Take into consideration any translationY added to the view.
+ int bottom = tempRect.bottom + Math.round(child.getTranslationY());
+ int top = bottom - thickness;
+ dividerDrawable.setBounds(left, top, right, bottom);
+ dividerDrawable.draw(canvas);
+ }
+ }
+ canvas.restore();
+ }
+
+ /**
+ * Draws a divider for the horizontal orientation of the recycler view. The divider itself will be
+ * vertical.
+ */
+ private void drawForHorizontalOrientation(@NonNull Canvas canvas, @NonNull RecyclerView parent) {
+ canvas.save();
+ int top;
+ int bottom;
+ if (parent.getClipToPadding()) {
+ top = parent.getPaddingTop();
+ bottom = parent.getHeight() - parent.getPaddingBottom();
+ canvas.clipRect(
+ parent.getPaddingLeft(), top, parent.getWidth() - parent.getPaddingRight(), bottom);
+ } else {
+ top = 0;
+ bottom = parent.getHeight();
+ }
+ top += insetStart;
+ bottom -= insetEnd;
+
+ int childCount = parent.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = parent.getChildAt(i);
+ if (shouldDrawDivider(parent, child)) {
+ parent.getDecoratedBoundsWithMargins(child, tempRect);
+ // Take into consideration any translationX added to the view.
+ int right = tempRect.right + Math.round(child.getTranslationX());
+ int left = right - thickness;
+ dividerDrawable.setBounds(left, top, right, bottom);
+ dividerDrawable.draw(canvas);
+ }
+ }
+ canvas.restore();
+ }
+
+ @Override
+ public void getItemOffsets(
+ @NonNull Rect outRect,
+ @NonNull View view,
+ @NonNull RecyclerView parent,
+ @NonNull RecyclerView.State state) {
+ outRect.set(0, 0, 0, 0);
+ // Only add offset if there's a divider displayed.
+ if (shouldDrawDivider(parent, view)) {
+ if (orientation == VERTICAL) {
+ outRect.bottom = thickness;
+ } else {
+ outRect.right = thickness;
+ }
+ }
+ }
+
+ private boolean shouldDrawDivider(@NonNull RecyclerView parent, @NonNull View child) {
+ int position = parent.getChildAdapterPosition(child);
+ RecyclerView.Adapter> adapter = parent.getAdapter();
+ boolean isLastItem = adapter != null && position == adapter.getItemCount() - 1;
+
+ return position != RecyclerView.NO_POSITION
+ && (!isLastItem || lastItemDecorated)
+ && shouldDrawDivider(position, adapter);
+ }
+
+ /**
+ * Whether a divider should be drawn below the current item that is being drawn.
+ *
+ *
Note: if lasItemDecorated is false, the divider below the last item will never be drawn even
+ * if this method returns true.
+ *
+ * @param position the position of the current item being drawn.
+ * @param adapter the {@link RecyclerView.Adapter} associated with the item being drawn.
+ */
+ protected boolean shouldDrawDivider(int position, @Nullable RecyclerView.Adapter> adapter) {
+ return true;
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt b/app/src/main/java/org/oxycblt/auxio/Auxio.kt
similarity index 90%
rename from app/src/main/java/org/oxycblt/auxio/AuxioApp.kt
rename to app/src/main/java/org/oxycblt/auxio/Auxio.kt
index 1e645d506..bcb8777e8 100644
--- a/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt
+++ b/app/src/main/java/org/oxycblt/auxio/Auxio.kt
@@ -25,22 +25,26 @@ import androidx.core.graphics.drawable.IconCompat
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.request.CachePolicy
+import org.oxycblt.auxio.image.ImageSettings
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.settings.Settings
+import org.oxycblt.auxio.playback.PlaybackSettings
+import org.oxycblt.auxio.ui.UISettings
/**
- * Auxio: A simple, rational music player for android.
+ * A simple, rational music player for android.
* @author Alexander Capehart (OxygenCobalt)
*/
-class AuxioApp : Application(), ImageLoaderFactory {
+class Auxio : Application(), ImageLoaderFactory {
override fun onCreate() {
super.onCreate()
// Migrate any settings that may have changed in an app update.
- Settings(this).migrate()
+ ImageSettings.from(this).migrate()
+ PlaybackSettings.from(this).migrate()
+ UISettings.from(this).migrate()
// Adding static shortcuts in a dynamic manner is better than declaring them
// manually, as it will properly handle the difference between debug and release
// Auxio instances.
diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt
index 25d40c62c..201abbd18 100644
--- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt
+++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt
@@ -29,7 +29,7 @@ import org.oxycblt.auxio.music.system.IndexerService
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.InternalPlayer
import org.oxycblt.auxio.playback.system.PlaybackService
-import org.oxycblt.auxio.settings.Settings
+import org.oxycblt.auxio.ui.UISettings
import org.oxycblt.auxio.util.androidViewModels
import org.oxycblt.auxio.util.isNight
import org.oxycblt.auxio.util.logD
@@ -81,7 +81,7 @@ class MainActivity : AppCompatActivity() {
}
private fun setupTheme() {
- val settings = Settings(this)
+ val settings = UISettings.from(this)
// Apply the theme configuration.
AppCompatDelegate.setDefaultNightMode(settings.theme)
// Apply the color scheme. The black theme requires it's own set of themes since
@@ -131,7 +131,7 @@ class MainActivity : AppCompatActivity() {
val action =
when (intent.action) {
Intent.ACTION_VIEW -> InternalPlayer.Action.Open(intent.data ?: return false)
- AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll
+ Auxio.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll
else -> return false
}
playbackModel.startAction(action)
diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt
index c3a3060ce..8b93763f5 100644
--- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt
@@ -30,7 +30,7 @@ import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
-import com.google.android.material.bottomsheet.NeoBottomSheetBehavior
+import com.google.android.material.bottomsheet.BackportBottomSheetBehavior
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.transition.MaterialFadeThrough
import kotlin.math.max
@@ -101,10 +101,10 @@ class MainFragment :
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
unlikelyToBeNull(binding.handleWrapper).setOnClickListener {
- if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED &&
- queueSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED) {
+ if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED &&
+ queueSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED) {
// Playback sheet is expanded and queue sheet is collapsed, we can expand it.
- queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_EXPANDED
+ queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_EXPANDED
}
}
} else {
@@ -183,7 +183,7 @@ class MainFragment :
// Playback sheet intercepts queue sheet touch events, prevent that from
// occurring by disabling dragging whenever the queue sheet is expanded.
playbackSheetBehavior.isDraggable =
- queueSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED
+ queueSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED
}
} else {
// No queue sheet, fade normally based on the playback sheet
@@ -235,8 +235,8 @@ class MainFragment :
tryHideAllSheets()
}
- // Since the listener is also reliant on the bottom sheets, we must also update it
- // every frame.
+ // Since the navigation listener is also reliant on the bottom sheets, we must also update
+ // it every frame.
callback.invalidateEnabled()
return true
@@ -309,7 +309,7 @@ class MainFragment :
navModel.mainNavigateTo(
MainNavigationAction.Directions(
MainFragmentDirections.actionPickPlaybackGenre(song.uid)))
- playbackModel.finishPlaybackArtistPicker()
+ playbackModel.finishPlaybackGenrePicker()
}
}
@@ -317,9 +317,9 @@ class MainFragment :
val binding = requireBinding()
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
- if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED) {
+ if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED) {
// Playback sheet is not expanded and not hidden, we can expand it.
- playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_EXPANDED
+ playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_EXPANDED
}
}
@@ -327,12 +327,12 @@ class MainFragment :
val binding = requireBinding()
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
- if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED) {
+ if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) {
// Make sure the queue is also collapsed here.
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
- playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
- queueSheetBehavior?.state = NeoBottomSheetBehavior.STATE_COLLAPSED
+ playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED
+ queueSheetBehavior?.state = BackportBottomSheetBehavior.STATE_COLLAPSED
}
}
@@ -340,17 +340,15 @@ class MainFragment :
val binding = requireBinding()
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
- if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_HIDDEN) {
+ if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_HIDDEN) {
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
-
// Queue sheet behavior is either collapsed or expanded, no hiding needed
queueSheetBehavior?.isDraggable = true
-
playbackSheetBehavior.apply {
// Make sure the view is draggable, at least until the draw checks kick in.
isDraggable = true
- state = NeoBottomSheetBehavior.STATE_COLLAPSED
+ state = BackportBottomSheetBehavior.STATE_COLLAPSED
}
}
}
@@ -359,19 +357,19 @@ class MainFragment :
val binding = requireBinding()
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
- if (playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_HIDDEN) {
+ if (playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_HIDDEN) {
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
// Make both bottom sheets non-draggable so the user can't halt the hiding event.
queueSheetBehavior?.apply {
isDraggable = false
- state = NeoBottomSheetBehavior.STATE_COLLAPSED
+ state = BackportBottomSheetBehavior.STATE_COLLAPSED
}
playbackSheetBehavior.apply {
isDraggable = false
- state = NeoBottomSheetBehavior.STATE_HIDDEN
+ state = BackportBottomSheetBehavior.STATE_HIDDEN
}
}
}
@@ -390,16 +388,21 @@ class MainFragment :
// If expanded, collapse the queue sheet first.
if (queueSheetBehavior != null &&
- queueSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED &&
- playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED) {
- queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
+ queueSheetBehavior.state != BackportBottomSheetBehavior.STATE_COLLAPSED &&
+ playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) {
+ queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED
return
}
// If expanded, collapse the playback sheet next.
- if (playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED &&
- playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_HIDDEN) {
- playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
+ if (playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_COLLAPSED &&
+ playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_HIDDEN) {
+ playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED
+ return
+ }
+
+ // Clear out any prior selections.
+ if (selectionModel.consume().isNotEmpty()) {
return
}
@@ -425,8 +428,9 @@ class MainFragment :
val exploreNavController = binding.exploreNavHost.findNavController()
isEnabled =
- queueSheetBehavior?.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
- playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
+ queueSheetBehavior?.state == BackportBottomSheetBehavior.STATE_EXPANDED ||
+ playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED ||
+ selectionModel.selected.value.isNotEmpty() ||
exploreNavController.currentDestination?.id !=
exploreNavController.graph.startDestinationId
}
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt
index 1d3d4f9f3..90689c188 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt
@@ -31,26 +31,22 @@ import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment
+import org.oxycblt.auxio.list.adapter.BasicListInstructions
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
-import org.oxycblt.auxio.music.Sort
-import org.oxycblt.auxio.settings.Settings
-import org.oxycblt.auxio.util.canScroll
-import org.oxycblt.auxio.util.collect
-import org.oxycblt.auxio.util.collectImmediately
-import org.oxycblt.auxio.util.logD
-import org.oxycblt.auxio.util.showToast
-import org.oxycblt.auxio.util.unlikelyToBeNull
+import org.oxycblt.auxio.music.library.Sort
+import org.oxycblt.auxio.util.*
/**
* A [ListFragment] that shows information about an [Album].
* @author Alexander Capehart (OxygenCobalt)
*/
-class AlbumDetailFragment : ListFragment(), AlbumDetailAdapter.Listener {
+class AlbumDetailFragment :
+ ListFragment(), AlbumDetailAdapter.Listener {
private val detailModel: DetailViewModel by activityViewModels()
// Information about what album to display is initially within the navigation arguments
// as a UID, as that is the only safe way to parcel an album.
@@ -88,7 +84,7 @@ class AlbumDetailFragment : ListFragment(), AlbumDetailAd
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setAlbumUid(args.albumUid)
collectImmediately(detailModel.currentAlbum, ::updateAlbum)
- collectImmediately(detailModel.albumList, detailAdapter::submitList)
+ collectImmediately(detailModel.albumList, ::updateList)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation)
@@ -126,21 +122,12 @@ class AlbumDetailFragment : ListFragment(), AlbumDetailAd
}
}
- override fun onRealClick(music: Music) {
- check(music is Song) { "Unexpected datatype: ${music::class.java}" }
- when (Settings(requireContext()).detailPlaybackMode) {
- // "Play from shown item" and "Play from album" functionally have the same
- // behavior since a song can only have one album.
- null,
- MusicMode.ALBUMS -> playbackModel.playFromAlbum(music)
- MusicMode.SONGS -> playbackModel.playFromAll(music)
- MusicMode.ARTISTS -> playbackModel.playFromArtist(music)
- MusicMode.GENRES -> playbackModel.playFromGenre(music)
- }
+ override fun onRealClick(item: Song) {
+ // There can only be one album, so a null mode and an ALBUMS mode will function the same.
+ playbackModel.playFrom(item, detailModel.playbackMode ?: MusicMode.ALBUMS)
}
- override fun onOpenMenu(item: Item, anchor: View) {
- check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" }
+ override fun onOpenMenu(item: Song, anchor: View) {
openMusicMenu(anchor, R.menu.menu_album_song_actions, item)
}
@@ -154,12 +141,12 @@ class AlbumDetailFragment : ListFragment(), AlbumDetailAd
override fun onOpenSortMenu(anchor: View) {
openMenu(anchor, R.menu.menu_album_sort) {
- val sort = detailModel.albumSort
+ val sort = detailModel.albumSongSort
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
setOnMenuItemClickListener { item ->
item.isChecked = !item.isChecked
- detailModel.albumSort =
+ detailModel.albumSongSort =
if (item.itemId == R.id.option_sort_asc) {
sort.withAscending(item.isChecked)
} else {
@@ -185,10 +172,10 @@ class AlbumDetailFragment : ListFragment(), AlbumDetailAd
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) {
- detailAdapter.setPlayingItem(song, isPlaying)
+ detailAdapter.setPlaying(song, isPlaying)
} else {
// Clear the ViewHolders if the mode isn't ALL_SONGS
- detailAdapter.setPlayingItem(null, isPlaying)
+ detailAdapter.setPlaying(null, isPlaying)
}
}
@@ -272,8 +259,12 @@ class AlbumDetailFragment : ListFragment(), AlbumDetailAd
}
}
+ private fun updateList(items: List- ) {
+ detailAdapter.submitList(items, BasicListInstructions.DIFF)
+ }
+
private fun updateSelection(selected: List) {
- detailAdapter.setSelectedItems(selected)
+ detailAdapter.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 e1d103861..5734ee1a8 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt
@@ -31,14 +31,13 @@ import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
import org.oxycblt.auxio.detail.recycler.DetailAdapter
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment
+import org.oxycblt.auxio.list.adapter.BasicListInstructions
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
-import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
-import org.oxycblt.auxio.music.Sort
-import org.oxycblt.auxio.settings.Settings
+import org.oxycblt.auxio.music.library.Sort
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
@@ -49,7 +48,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* A [ListFragment] that shows information about an [Artist].
* @author Alexander Capehart (OxygenCobalt)
*/
-class ArtistDetailFragment : ListFragment(), DetailAdapter.Listener {
+class ArtistDetailFragment :
+ ListFragment(), DetailAdapter.Listener {
private val detailModel: DetailViewModel by activityViewModels()
// 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.
@@ -87,7 +87,7 @@ class ArtistDetailFragment : ListFragment(), DetailAdapte
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setArtistUid(args.artistUid)
collectImmediately(detailModel.currentArtist, ::updateItem)
- collectImmediately(detailModel.artistList, detailAdapter::submitList)
+ collectImmediately(detailModel.artistList, ::updateList)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation)
@@ -121,27 +121,25 @@ class ArtistDetailFragment : ListFragment(), DetailAdapte
}
}
- override fun onRealClick(music: Music) {
- when (music) {
+ override fun onRealClick(item: Music) {
+ when (item) {
+ is Album -> navModel.exploreNavigateTo(item)
is Song -> {
- when (Settings(requireContext()).detailPlaybackMode) {
+ val playbackMode = detailModel.playbackMode
+ if (playbackMode != null) {
+ playbackModel.playFrom(item, playbackMode)
+ } else {
// When configured to play from the selected item, we already have an Artist
// to play from.
- null ->
- playbackModel.playFromArtist(
- music, unlikelyToBeNull(detailModel.currentArtist.value))
- MusicMode.SONGS -> playbackModel.playFromAll(music)
- MusicMode.ALBUMS -> playbackModel.playFromAlbum(music)
- MusicMode.ARTISTS -> playbackModel.playFromArtist(music)
- MusicMode.GENRES -> playbackModel.playFromGenre(music)
+ playbackModel.playFromArtist(
+ item, unlikelyToBeNull(detailModel.currentArtist.value))
}
}
- is Album -> navModel.exploreNavigateTo(music)
- else -> error("Unexpected datatype: ${music::class.simpleName}")
+ else -> error("Unexpected datatype: ${item::class.simpleName}")
}
}
- override fun onOpenMenu(item: Item, anchor: View) {
+ override fun onOpenMenu(item: Music, anchor: View) {
when (item) {
is Song -> openMusicMenu(anchor, R.menu.menu_artist_song_actions, item)
is Album -> openMusicMenu(anchor, R.menu.menu_artist_album_actions, item)
@@ -159,13 +157,13 @@ class ArtistDetailFragment : ListFragment(), DetailAdapte
override fun onOpenSortMenu(anchor: View) {
openMenu(anchor, R.menu.menu_artist_sort) {
- val sort = detailModel.artistSort
+ val sort = detailModel.artistSongSort
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
setOnMenuItemClickListener { item ->
item.isChecked = !item.isChecked
- detailModel.artistSort =
+ detailModel.artistSongSort =
if (item.itemId == R.id.option_sort_asc) {
sort.withAscending(item.isChecked)
} else {
@@ -199,7 +197,7 @@ class ArtistDetailFragment : ListFragment(), DetailAdapte
else -> null
}
- detailAdapter.setPlayingItem(playingItem, isPlaying)
+ detailAdapter.setPlaying(playingItem, isPlaying)
}
private fun handleNavigation(item: Music?) {
@@ -237,8 +235,12 @@ class ArtistDetailFragment : ListFragment(), DetailAdapte
}
}
+ private fun updateList(items: List
- ) {
+ detailAdapter.submitList(items, BasicListInstructions.DIFF)
+ }
+
private fun updateSelection(selected: List) {
- detailAdapter.setSelectedItems(selected)
+ detailAdapter.setSelected(selected.toSet())
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt b/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt
index d5b32e584..1788b4ddd 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt
@@ -20,17 +20,19 @@ package org.oxycblt.auxio.detail
import androidx.annotation.StringRes
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.Song
-import org.oxycblt.auxio.music.filesystem.MimeType
+import org.oxycblt.auxio.music.storage.MimeType
/**
* 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)
*/
data class SortHeader(@StringRes val titleRes: Int) : Item
/**
* A header variation that delimits between disc groups.
* @param disc The disc number to be displayed on the header.
+ * @author Alexander Capehart (OxygenCobalt)
*/
data class DiscHeader(val disc: Int) : Item
@@ -39,6 +41,7 @@ data class DiscHeader(val disc: Int) : Item
* @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.
+ * @author Alexander Capehart (OxygenCobalt)
*/
data class SongProperties(
val bitrateKbps: Int?,
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 2f7a404e0..d51cd3734 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt
@@ -32,15 +32,13 @@ import kotlinx.coroutines.yield
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item
-import org.oxycblt.auxio.music.Album
-import org.oxycblt.auxio.music.Artist
-import org.oxycblt.auxio.music.Genre
-import org.oxycblt.auxio.music.Music
+import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.MusicStore
-import org.oxycblt.auxio.music.Song
-import org.oxycblt.auxio.music.Sort
-import org.oxycblt.auxio.music.filesystem.MimeType
-import org.oxycblt.auxio.settings.Settings
+import org.oxycblt.auxio.music.library.Library
+import org.oxycblt.auxio.music.library.Sort
+import org.oxycblt.auxio.music.storage.MimeType
+import org.oxycblt.auxio.music.tags.ReleaseType
+import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.util.*
/**
@@ -53,7 +51,8 @@ import org.oxycblt.auxio.util.*
class DetailViewModel(application: Application) :
AndroidViewModel(application), MusicStore.Listener {
private val musicStore = MusicStore.getInstance()
- private val settings = Settings(application)
+ private val musicSettings = MusicSettings.from(application)
+ private val playbackSettings = PlaybackSettings.from(application)
private var currentSongJob: Job? = null
@@ -81,10 +80,10 @@ class DetailViewModel(application: Application) :
get() = _albumList
/** The current [Sort] used for [Song]s in [albumList]. */
- var albumSort: Sort
- get() = settings.detailAlbumSort
+ var albumSongSort: Sort
+ get() = musicSettings.albumSongSort
set(value) {
- settings.detailAlbumSort = value
+ musicSettings.albumSongSort = value
// Refresh the album list to reflect the new sort.
currentAlbum.value?.let(::refreshAlbumList)
}
@@ -101,10 +100,10 @@ class DetailViewModel(application: Application) :
val artistList: StateFlow
> = _artistList
/** The current [Sort] used for [Song]s in [artistList]. */
- var artistSort: Sort
- get() = settings.detailArtistSort
+ var artistSongSort: Sort
+ get() = musicSettings.artistSongSort
set(value) {
- settings.detailArtistSort = value
+ musicSettings.artistSongSort = value
// Refresh the artist list to reflect the new sort.
currentArtist.value?.let(::refreshArtistList)
}
@@ -121,14 +120,21 @@ class DetailViewModel(application: Application) :
val genreList: StateFlow> = _genreList
/** The current [Sort] used for [Song]s in [genreList]. */
- var genreSort: Sort
- get() = settings.detailGenreSort
+ var genreSongSort: Sort
+ get() = musicSettings.genreSongSort
set(value) {
- settings.detailGenreSort = value
+ musicSettings.genreSongSort = value
// Refresh the genre list to reflect the new sort.
currentGenre.value?.let(::refreshGenreList)
}
+ /**
+ * The [MusicMode] to use when playing a [Song] from the UI, or null to play from the currently
+ * shown item.
+ */
+ val playbackMode: MusicMode?
+ get() = playbackSettings.inParentPlaybackMode
+
init {
musicStore.addListener(this)
}
@@ -137,7 +143,7 @@ class DetailViewModel(application: Application) :
musicStore.removeListener(this)
}
- override fun onLibraryChanged(library: MusicStore.Library?) {
+ override fun onLibraryChanged(library: Library?) {
if (library == null) {
// Nothing to do.
return
@@ -173,8 +179,8 @@ class DetailViewModel(application: Application) :
}
/**
- * Set a new [currentSong] from it's [Music.UID]. If the [Music.UID] differs, [currentSong]
- * and [songProperties] will be updated to align with the new [Song].
+ * Set a new [currentSong] from it's [Music.UID]. If the [Music.UID] differs, [currentSong] and
+ * [songProperties] 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) {
@@ -315,7 +321,7 @@ class DetailViewModel(application: Application) :
// 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.
- val songs = albumSort.songs(album.songs)
+ val songs = albumSongSort.songs(album.songs)
// Songs without disc tags become part of Disc 1.
val byDisc = songs.groupBy { it.disc ?: 1 }
if (byDisc.size > 1) {
@@ -339,21 +345,21 @@ class DetailViewModel(application: Application) :
val byReleaseGroup =
albums.groupBy {
- // Remap the complicated Album.Type data structure into an easier
+ // Remap the complicated ReleaseType data structure into an easier
// "AlbumGrouping" enum that will automatically group and sort
// the artist's albums.
- when (it.type.refinement) {
- Album.Type.Refinement.LIVE -> AlbumGrouping.LIVE
- Album.Type.Refinement.REMIX -> AlbumGrouping.REMIXES
+ when (it.releaseType.refinement) {
+ ReleaseType.Refinement.LIVE -> AlbumGrouping.LIVE
+ ReleaseType.Refinement.REMIX -> AlbumGrouping.REMIXES
null ->
- when (it.type) {
- is Album.Type.Album -> AlbumGrouping.ALBUMS
- is Album.Type.EP -> AlbumGrouping.EPS
- is Album.Type.Single -> AlbumGrouping.SINGLES
- is Album.Type.Compilation -> AlbumGrouping.COMPILATIONS
- is Album.Type.Soundtrack -> AlbumGrouping.SOUNDTRACKS
- is Album.Type.Mix -> AlbumGrouping.MIXES
- is Album.Type.Mixtape -> AlbumGrouping.MIXTAPES
+ when (it.releaseType) {
+ is ReleaseType.Album -> AlbumGrouping.ALBUMS
+ is ReleaseType.EP -> AlbumGrouping.EPS
+ is ReleaseType.Single -> AlbumGrouping.SINGLES
+ is ReleaseType.Compilation -> AlbumGrouping.COMPILATIONS
+ is ReleaseType.Soundtrack -> AlbumGrouping.SOUNDTRACKS
+ is ReleaseType.Mix -> AlbumGrouping.MIXES
+ is ReleaseType.Mixtape -> AlbumGrouping.MIXTAPES
}
}
}
@@ -369,7 +375,7 @@ class DetailViewModel(application: Application) :
if (artist.songs.isNotEmpty()) {
logD("Songs present in this artist, adding header")
data.add(SortHeader(R.string.lbl_songs))
- data.addAll(artistSort.songs(artist.songs))
+ data.addAll(artistSongSort.songs(artist.songs))
}
_artistList.value = data.toList()
@@ -382,12 +388,12 @@ class DetailViewModel(application: Application) :
data.add(Header(R.string.lbl_artists))
data.addAll(genre.artists)
data.add(SortHeader(R.string.lbl_songs))
- data.addAll(genreSort.songs(genre.songs))
+ data.addAll(genreSongSort.songs(genre.songs))
_genreList.value = data
}
/**
- * A simpler mapping of [Album.Type] used for grouping and sorting songs.
+ * 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.
*/
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 bfeca52c3..5d6ac4482 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt
@@ -31,15 +31,14 @@ import org.oxycblt.auxio.detail.recycler.DetailAdapter
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment
+import org.oxycblt.auxio.list.adapter.BasicListInstructions
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
-import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
-import org.oxycblt.auxio.music.Sort
-import org.oxycblt.auxio.settings.Settings
+import org.oxycblt.auxio.music.library.Sort
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
@@ -50,7 +49,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* A [ListFragment] that shows information for a particular [Genre].
* @author Alexander Capehart (OxygenCobalt)
*/
-class GenreDetailFragment : ListFragment(), DetailAdapter.Listener {
+class GenreDetailFragment :
+ ListFragment(), DetailAdapter.Listener {
private val detailModel: DetailViewModel by activityViewModels()
// 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.
@@ -86,7 +86,7 @@ class GenreDetailFragment : ListFragment(), DetailAdapter
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setGenreUid(args.genreUid)
collectImmediately(detailModel.currentGenre, ::updateItem)
- collectImmediately(detailModel.genreList, detailAdapter::submitList)
+ collectImmediately(detailModel.genreList, ::updateList)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation)
@@ -120,26 +120,25 @@ class GenreDetailFragment : ListFragment(), DetailAdapter
}
}
- override fun onRealClick(music: Music) {
- when (music) {
- is Artist -> navModel.exploreNavigateTo(music)
- is Song ->
- when (Settings(requireContext()).detailPlaybackMode) {
- // When configured to play from the selected item, we already have a Genre
+ override fun onRealClick(item: Music) {
+ when (item) {
+ is Artist -> navModel.exploreNavigateTo(item)
+ is Song -> {
+ val playbackMode = detailModel.playbackMode
+ if (playbackMode != null) {
+ playbackModel.playFrom(item, playbackMode)
+ } else {
+ // When configured to play from the selected item, we already have an Genre
// to play from.
- null ->
- playbackModel.playFromGenre(
- music, unlikelyToBeNull(detailModel.currentGenre.value))
- MusicMode.SONGS -> playbackModel.playFromAll(music)
- MusicMode.ALBUMS -> playbackModel.playFromAlbum(music)
- MusicMode.ARTISTS -> playbackModel.playFromArtist(music)
- MusicMode.GENRES -> playbackModel.playFromGenre(music)
+ playbackModel.playFromGenre(
+ item, unlikelyToBeNull(detailModel.currentGenre.value))
}
- else -> error("Unexpected datatype: ${music::class.simpleName}")
+ }
+ else -> error("Unexpected datatype: ${item::class.simpleName}")
}
}
- override fun onOpenMenu(item: Item, anchor: View) {
+ override fun onOpenMenu(item: Music, anchor: View) {
when (item) {
is Artist -> openMusicMenu(anchor, R.menu.menu_artist_actions, item)
is Song -> openMusicMenu(anchor, R.menu.menu_song_actions, item)
@@ -157,12 +156,12 @@ class GenreDetailFragment : ListFragment(), DetailAdapter
override fun onOpenSortMenu(anchor: View) {
openMenu(anchor, R.menu.menu_genre_sort) {
- val sort = detailModel.genreSort
+ val sort = detailModel.genreSongSort
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
setOnMenuItemClickListener { item ->
item.isChecked = !item.isChecked
- detailModel.genreSort =
+ detailModel.genreSongSort =
if (item.itemId == R.id.option_sort_asc) {
sort.withAscending(item.isChecked)
} else {
@@ -184,17 +183,15 @@ class GenreDetailFragment : ListFragment(), DetailAdapter
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
- var item: Item? = null
-
+ var playingMusic: Music? = null
if (parent is Artist) {
- item = parent
+ playingMusic = parent
}
-
+ // Prefer songs that might be playing from this genre.
if (parent is Genre && parent.uid == unlikelyToBeNull(detailModel.currentGenre.value).uid) {
- item = song
+ playingMusic = song
}
-
- detailAdapter.setPlayingItem(item, isPlaying)
+ detailAdapter.setPlaying(playingMusic, isPlaying)
}
private fun handleNavigation(item: Music?) {
@@ -221,8 +218,12 @@ class GenreDetailFragment : ListFragment(), DetailAdapter
}
}
+ private fun updateList(items: List- ) {
+ detailAdapter.submitList(items, BasicListInstructions.DIFF)
+ }
+
private fun updateSelection(selected: List) {
- detailAdapter.setSelectedItems(selected)
+ detailAdapter.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 327038255..f0e0924e0 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt
@@ -30,7 +30,7 @@ import org.oxycblt.auxio.R
*
* Adapted from Material Files: https://github.com/zhanghai/MaterialFiles
*
- * @author Alexander Capehart (OxygenCobalt)
+ * @author Hai Zhang, Alexander Capehart (OxygenCobalt)
*/
class ReadOnlyTextInput
@JvmOverloads
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt
index 8cc214b14..6a8611cb7 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt
@@ -29,8 +29,8 @@ import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
import org.oxycblt.auxio.detail.DiscHeader
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.SelectableListListener
-import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
-import org.oxycblt.auxio.list.recycler.SimpleItemCallback
+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.playback.formatDurationMs
@@ -48,7 +48,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
* An extension to [DetailAdapter.Listener] that enables interactions specific to the album
* detail view.
*/
- interface Listener : DetailAdapter.Listener {
+ interface Listener : DetailAdapter.Listener {
/**
* Called when the artist name in the [Album] header was clicked, requesting navigation to
* it's parent artist.
@@ -57,7 +57,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
}
override fun getItemViewType(position: Int) =
- when (differ.currentList[position]) {
+ when (getItem(position)) {
// Support the Album header, sub-headers for each disc, and special album songs.
is Album -> AlbumDetailViewHolder.VIEW_TYPE
is DiscHeader -> DiscHeaderViewHolder.VIEW_TYPE
@@ -75,7 +75,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
- when (val item = differ.currentList[position]) {
+ when (val item = getItem(position)) {
is Album -> (holder as AlbumDetailViewHolder).bind(item, listener)
is DiscHeader -> (holder as DiscHeaderViewHolder).bind(item)
is Song -> (holder as AlbumSongViewHolder).bind(item, listener)
@@ -83,15 +83,18 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
}
override fun isItemFullWidth(position: Int): Boolean {
+ if (super.isItemFullWidth(position)) {
+ return true
+ }
// The album and disc headers should be full-width in all configurations.
- val item = differ.currentList[position]
- return super.isItemFullWidth(position) || item is Album || item is DiscHeader
+ val item = getItem(position)
+ return item is Album || item is DiscHeader
}
private companion object {
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
- object : SimpleItemCallback
- () {
+ object : SimpleDiffCallback
- () {
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return when {
oldItem is Album && newItem is Album ->
@@ -126,7 +129,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
binding.detailCover.bind(album)
// The type text depends on the release type (Album, EP, Single, etc.)
- binding.detailType.text = binding.context.getString(album.type.stringRes)
+ binding.detailType.text = binding.context.getString(album.releaseType.stringRes)
binding.detailName.text = album.resolveName(binding.context)
@@ -166,14 +169,14 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
- object : SimpleItemCallback() {
+ object : SimpleDiffCallback() {
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
oldItem.rawName == newItem.rawName &&
oldItem.areArtistContentsTheSame(newItem) &&
oldItem.dates == newItem.dates &&
oldItem.songs.size == newItem.songs.size &&
oldItem.durationMs == newItem.durationMs &&
- oldItem.type == newItem.type
+ oldItem.releaseType == newItem.releaseType
}
}
}
@@ -207,7 +210,7 @@ private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
- object : SimpleItemCallback() {
+ object : SimpleDiffCallback() {
override fun areContentsTheSame(oldItem: DiscHeader, newItem: DiscHeader) =
oldItem.disc == newItem.disc
}
@@ -226,7 +229,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
* @param song The new [Song] to bind.
* @param listener A [SelectableListListener] to bind interactions to.
*/
- fun bind(song: Song, listener: SelectableListListener) {
+ fun bind(song: Song, listener: SelectableListListener) {
listener.bind(song, this, menuButton = binding.songMenu)
binding.songTrack.apply {
@@ -274,7 +277,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
- object : SimpleItemCallback() {
+ object : SimpleDiffCallback() {
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
oldItem.rawName == newItem.rawName && oldItem.durationMs == newItem.durationMs
}
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt
index 84aedabfb..e706efc23 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt
@@ -28,10 +28,11 @@ import org.oxycblt.auxio.databinding.ItemParentBinding
import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.SelectableListListener
-import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
-import org.oxycblt.auxio.list.recycler.SimpleItemCallback
+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.Artist
+import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getPlural
@@ -42,9 +43,10 @@ import org.oxycblt.auxio.util.inflater
* @param listener A [DetailAdapter.Listener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt)
*/
-class ArtistDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) {
+class ArtistDetailAdapter(private val listener: Listener) :
+ DetailAdapter(listener, DIFF_CALLBACK) {
override fun getItemViewType(position: Int) =
- when (differ.currentList[position]) {
+ when (getItem(position)) {
// Support an artist header, and special artist albums/songs.
is Artist -> ArtistDetailViewHolder.VIEW_TYPE
is Album -> ArtistAlbumViewHolder.VIEW_TYPE
@@ -63,7 +65,7 @@ class ArtistDetailAdapter(private val listener: Listener) : DetailAdapter(listen
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
// Re-binding an item with new data and not just a changed selection/playing state.
- when (val item = differ.currentList[position]) {
+ 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)
@@ -71,15 +73,17 @@ class ArtistDetailAdapter(private val listener: Listener) : DetailAdapter(listen
}
override fun isItemFullWidth(position: Int): Boolean {
+ if (super.isItemFullWidth(position)) {
+ return true
+ }
// Artist headers should be full-width in all configurations.
- val item = differ.currentList[position]
- return super.isItemFullWidth(position) || item is Artist
+ return getItem(position) is Artist
}
private companion object {
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
- object : SimpleItemCallback
- () {
+ object : SimpleDiffCallback
- () {
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return when {
oldItem is Artist && newItem is Artist ->
@@ -109,7 +113,7 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
* @param artist The new [Artist] to bind.
* @param listener A [DetailAdapter.Listener] to bind interactions to.
*/
- fun bind(artist: Artist, listener: DetailAdapter.Listener) {
+ 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)
@@ -161,7 +165,7 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
- object : SimpleItemCallback() {
+ object : SimpleDiffCallback() {
override fun areContentsTheSame(oldItem: Artist, newItem: Artist) =
oldItem.rawName == newItem.rawName &&
oldItem.areGenreContentsTheSame(newItem) &&
@@ -183,7 +187,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
* @param album The new [Album] to bind.
* @param listener An [SelectableListListener] to bind interactions to.
*/
- fun bind(album: Album, listener: SelectableListListener) {
+ fun bind(album: Album, listener: SelectableListListener) {
listener.bind(album, this, menuButton = binding.parentMenu)
binding.parentImage.bind(album)
binding.parentName.text = album.resolveName(binding.context)
@@ -216,7 +220,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
- object : SimpleItemCallback() {
+ object : SimpleDiffCallback() {
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
oldItem.rawName == newItem.rawName && oldItem.dates == newItem.dates
}
@@ -235,7 +239,7 @@ private class ArtistSongViewHolder private constructor(private val binding: Item
* @param song The new [Song] to bind.
* @param listener An [SelectableListListener] to bind interactions to.
*/
- fun bind(song: Song, listener: SelectableListListener) {
+ fun bind(song: Song, listener: SelectableListListener) {
listener.bind(song, this, menuButton = binding.songMenu)
binding.songAlbumCover.bind(song)
binding.songName.text = song.resolveName(binding.context)
@@ -265,7 +269,7 @@ private class ArtistSongViewHolder private constructor(private val binding: Item
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
- object : SimpleItemCallback() {
+ object : SimpleDiffCallback() {
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
oldItem.rawName == newItem.rawName &&
oldItem.album.rawName == newItem.album.rawName
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt
index 8775f4b9b..2ce12a786 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt
@@ -20,7 +20,6 @@ package org.oxycblt.auxio.detail.recycler
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.TooltipCompat
-import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable
@@ -29,26 +28,29 @@ import org.oxycblt.auxio.detail.SortHeader
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.SelectableListListener
+import org.oxycblt.auxio.list.adapter.*
import org.oxycblt.auxio.list.recycler.*
+import org.oxycblt.auxio.music.Music
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.
* @param listener A [Listener] to bind interactions to.
- * @param itemCallback A [DiffUtil.ItemCallback] to use with [AsyncListDiffer] when updating the
+ * @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
* internal list.
* @author Alexander Capehart (OxygenCobalt)
*/
abstract class DetailAdapter(
- private val listener: Listener,
- itemCallback: DiffUtil.ItemCallback
-
-) : SelectionIndicatorAdapter(), AuxioRecyclerView.SpanSizeLookup {
- // Safe to leak this since the listener will not fire during initialization
- @Suppress("LeakingThis") protected val differ = AsyncListDiffer(this, itemCallback)
+ private val listener: Listener<*>,
+ diffCallback: DiffUtil.ItemCallback
-
+) :
+ SelectionIndicatorAdapter
- (
+ ListDiffer.Async(diffCallback)),
+ AuxioRecyclerView.SpanSizeLookup {
override fun getItemViewType(position: Int) =
- when (differ.currentList[position]) {
+ when (getItem(position)) {
// Implement support for headers and sort headers
is Header -> HeaderViewHolder.VIEW_TYPE
is SortHeader -> SortHeaderViewHolder.VIEW_TYPE
@@ -63,7 +65,7 @@ abstract class DetailAdapter(
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
- when (val item = differ.currentList[position]) {
+ when (val item = getItem(position)) {
is Header -> (holder as HeaderViewHolder).bind(item)
is SortHeader -> (holder as SortHeaderViewHolder).bind(item, listener)
}
@@ -71,24 +73,12 @@ abstract class DetailAdapter(
override fun isItemFullWidth(position: Int): Boolean {
// Headers should be full-width in all configurations.
- val item = differ.currentList[position]
+ val item = getItem(position)
return item is Header || item is SortHeader
}
- override val currentList: List
-
- get() = differ.currentList
-
- /**
- * Asynchronously update the list with new items. Assumes that the list only contains data
- * supported by the concrete [DetailAdapter] implementation.
- * @param newList The new [Item]s for the adapter to display.
- */
- fun submitList(newList: List
- ) {
- differ.submitList(newList)
- }
-
/** An extended [SelectableListListener] for [DetailAdapter] implementations. */
- interface Listener : SelectableListListener {
+ 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
@@ -112,7 +102,7 @@ abstract class DetailAdapter(
protected companion object {
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
- object : SimpleItemCallback
- () {
+ object : SimpleDiffCallback
- () {
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return when {
oldItem is Header && newItem is Header ->
@@ -138,7 +128,7 @@ private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
* @param sortHeader The new [SortHeader] to bind.
* @param listener An [DetailAdapter.Listener] to bind interactions to.
*/
- fun bind(sortHeader: SortHeader, listener: DetailAdapter.Listener) {
+ fun bind(sortHeader: SortHeader, listener: DetailAdapter.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
@@ -162,7 +152,7 @@ private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
- object : SimpleItemCallback() {
+ object : SimpleDiffCallback() {
override fun areContentsTheSame(oldItem: SortHeader, newItem: SortHeader) =
oldItem.titleRes == newItem.titleRes
}
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
index fad27aa8a..51a86f335 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt
@@ -25,11 +25,12 @@ 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.SimpleItemCallback
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
@@ -40,12 +41,13 @@ import org.oxycblt.auxio.util.inflater
* @param listener A [DetailAdapter.Listener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt)
*/
-class GenreDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) {
+class GenreDetailAdapter(private val listener: Listener) :
+ DetailAdapter(listener, DIFF_CALLBACK) {
override fun getItemViewType(position: Int) =
- when (differ.currentList[position]) {
+ when (getItem(position)) {
// Support the Genre header and generic Artist/Song items. There's nothing about
- // a genre that will make the artists/songs homogeneous, so it doesn't matter what we
- // use for their ViewHolders.
+ // 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
@@ -62,7 +64,7 @@ class GenreDetailAdapter(private val listener: Listener) : DetailAdapter(listene
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
- when (val item = differ.currentList[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)
@@ -70,14 +72,16 @@ class GenreDetailAdapter(private val listener: Listener) : DetailAdapter(listene
}
override fun isItemFullWidth(position: Int): Boolean {
+ if (super.isItemFullWidth(position)) {
+ return true
+ }
// Genre headers should be full-width in all configurations
- val item = differ.currentList[position]
- return super.isItemFullWidth(position) || item is Genre
+ return getItem(position) is Genre
}
private companion object {
val DIFF_CALLBACK =
- object : SimpleItemCallback
- () {
+ object : SimpleDiffCallback
- () {
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return when {
oldItem is Genre && newItem is Genre ->
@@ -105,7 +109,7 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
* @param genre The new [Song] to bind.
* @param listener A [DetailAdapter.Listener] to bind interactions to.
*/
- fun bind(genre: Genre, listener: DetailAdapter.Listener) {
+ 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)
@@ -135,7 +139,7 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
- object : SimpleItemCallback() {
+ object : SimpleDiffCallback() {
override fun areContentsTheSame(oldItem: Genre, newItem: Genre) =
oldItem.rawName == newItem.rawName &&
oldItem.songs.size == newItem.songs.size &&
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 efa3d86f6..39ae497f0 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
@@ -50,6 +50,8 @@ import org.oxycblt.auxio.home.list.SongListFragment
import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy
import org.oxycblt.auxio.list.selection.SelectionFragment
import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.library.Library
+import org.oxycblt.auxio.music.library.Sort
import org.oxycblt.auxio.music.system.Indexer
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
@@ -143,7 +145,7 @@ class HomeFragment :
// --- VIEWMODEL SETUP ---
collect(homeModel.shouldRecreate, ::handleRecreate)
collectImmediately(homeModel.currentTabMode, ::updateCurrentTab)
- collectImmediately(homeModel.songLists, homeModel.isFastScrolling, ::updateFab)
+ collectImmediately(homeModel.songsList, homeModel.isFastScrolling, ::updateFab)
collectImmediately(musicModel.indexerState, ::updateIndexerState)
collect(navModel.exploreNavigationItem, ::handleNavigation)
collectImmediately(selectionModel.selected, ::updateSelection)
@@ -333,10 +335,7 @@ class HomeFragment :
}
}
- private fun setupCompleteState(
- binding: FragmentHomeBinding,
- result: Result
- ) {
+ private fun setupCompleteState(binding: FragmentHomeBinding, result: Result) {
if (result.isSuccess) {
logD("Received ok response")
binding.homeFab.show()
diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt
new file mode 100644
index 000000000..c36e9d838
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.home
+
+import android.content.Context
+import androidx.core.content.edit
+import org.oxycblt.auxio.R
+import org.oxycblt.auxio.home.tabs.Tab
+import org.oxycblt.auxio.settings.Settings
+import org.oxycblt.auxio.util.unlikelyToBeNull
+
+/**
+ * User configuration specific to the home UI.
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+interface HomeSettings : Settings {
+ /** The tabs to show in the home UI. */
+ var homeTabs: Array
+ /** Whether to hide artists considered "collaborators" from the home UI. */
+ val shouldHideCollaborators: Boolean
+
+ interface Listener {
+ /** Called when the [homeTabs] configuration changes. */
+ fun onTabsChanged()
+ /** Called when the [shouldHideCollaborators] configuration changes. */
+ fun onHideCollaboratorsChanged()
+ }
+
+ private class Real(context: Context) : Settings.Real(context), HomeSettings {
+ override var homeTabs: Array
+ get() =
+ Tab.fromIntCode(
+ sharedPreferences.getInt(
+ getString(R.string.set_key_home_tabs), Tab.SEQUENCE_DEFAULT))
+ ?: unlikelyToBeNull(Tab.fromIntCode(Tab.SEQUENCE_DEFAULT))
+ set(value) {
+ sharedPreferences.edit {
+ putInt(getString(R.string.set_key_home_tabs), Tab.toIntCode(value))
+ apply()
+ }
+ }
+
+ override val shouldHideCollaborators: Boolean
+ get() =
+ sharedPreferences.getBoolean(getString(R.string.set_key_hide_collaborators), false)
+
+ override fun onSettingChanged(key: String, listener: Listener) {
+ when (key) {
+ getString(R.string.set_key_home_tabs) -> listener.onTabsChanged()
+ getString(R.string.set_key_hide_collaborators) ->
+ listener.onHideCollaboratorsChanged()
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * Get a framework-backed implementation.
+ * @param context [Context] required.
+ */
+ fun from(context: Context): HomeSettings = Real(context)
+ }
+}
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 f9e88e3ef..63e6058bd 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt
@@ -18,21 +18,15 @@
package org.oxycblt.auxio.home
import android.app.Application
-import android.content.SharedPreferences
import androidx.lifecycle.AndroidViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
-import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.Tab
-import org.oxycblt.auxio.music.Album
-import org.oxycblt.auxio.music.Artist
-import org.oxycblt.auxio.music.Genre
-import org.oxycblt.auxio.music.MusicMode
+import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.MusicStore
-import org.oxycblt.auxio.music.Song
-import org.oxycblt.auxio.music.Sort
-import org.oxycblt.auxio.settings.Settings
-import org.oxycblt.auxio.util.context
+import org.oxycblt.auxio.music.library.Library
+import org.oxycblt.auxio.music.library.Sort
+import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.util.logD
/**
@@ -40,15 +34,15 @@ import org.oxycblt.auxio.util.logD
* @author Alexander Capehart (OxygenCobalt)
*/
class HomeViewModel(application: Application) :
- AndroidViewModel(application),
- MusicStore.Listener,
- SharedPreferences.OnSharedPreferenceChangeListener {
+ AndroidViewModel(application), MusicStore.Listener, HomeSettings.Listener {
private val musicStore = MusicStore.getInstance()
- private val settings = Settings(application)
+ private val homeSettings = HomeSettings.from(application)
+ private val musicSettings = MusicSettings.from(application)
+ private val playbackSettings = PlaybackSettings.from(application)
private val _songsList = MutableStateFlow(listOf())
/** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */
- val songLists: StateFlow
>
+ val songsList: StateFlow>
get() = _songsList
private val _albumsLists = MutableStateFlow(listOf())
@@ -70,11 +64,15 @@ class HomeViewModel(application: Application) :
val genresList: StateFlow>
get() = _genresList
+ /** The [MusicMode] to use when playing a [Song] from the UI. */
+ val playbackMode: MusicMode
+ get() = playbackSettings.inListPlaybackMode
+
/**
* A list of [MusicMode] corresponding to the current [Tab] configuration, excluding invisible
* [Tab]s.
*/
- var currentTabModes: List = makeTabModes()
+ var currentTabModes = makeTabModes()
private set
private val _currentTabMode = MutableStateFlow(currentTabModes[0])
@@ -95,45 +93,82 @@ class HomeViewModel(application: Application) :
init {
musicStore.addListener(this)
- settings.addListener(this)
+ homeSettings.registerListener(this)
}
override fun onCleared() {
super.onCleared()
musicStore.removeListener(this)
- settings.removeListener(this)
+ homeSettings.unregisterListener(this)
}
- override fun onLibraryChanged(library: MusicStore.Library?) {
+ override fun onLibraryChanged(library: Library?) {
if (library != null) {
logD("Library changed, refreshing library")
// Get the each list of items in the library to use as our list data.
// Applying the preferred sorting to them.
- _songsList.value = settings.libSongSort.songs(library.songs)
- _albumsLists.value = settings.libAlbumSort.albums(library.albums)
+ _songsList.value = musicSettings.songSort.songs(library.songs)
+ _albumsLists.value = musicSettings.albumSort.albums(library.albums)
_artistsList.value =
- settings.libArtistSort.artists(
- if (settings.shouldHideCollaborators) {
+ musicSettings.artistSort.artists(
+ if (homeSettings.shouldHideCollaborators) {
// Hide Collaborators is enabled, filter out collaborators.
library.artists.filter { !it.isCollaborator }
} else {
library.artists
})
- _genresList.value = settings.libGenreSort.genres(library.genres)
+ _genresList.value = musicSettings.genreSort.genres(library.genres)
}
}
- override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
- when (key) {
- context.getString(R.string.set_key_lib_tabs) -> {
- // Tabs changed, update the current tabs and set up a re-create event.
- currentTabModes = makeTabModes()
- _shouldRecreate.value = true
+ override fun onTabsChanged() {
+ // Tabs changed, update the current tabs and set up a re-create event.
+ currentTabModes = makeTabModes()
+ _shouldRecreate.value = true
+ }
+
+ override fun onHideCollaboratorsChanged() {
+ // Changes in the hide collaborator setting will change the artist contents
+ // of the library, consider it a library update.
+ onLibraryChanged(musicStore.library)
+ }
+
+ /**
+ * Get the preferred [Sort] for a given [Tab].
+ * @param tabMode The [MusicMode] of the [Tab] desired.
+ * @return The [Sort] preferred for that [Tab]
+ */
+ fun getSortForTab(tabMode: MusicMode) =
+ when (tabMode) {
+ MusicMode.SONGS -> musicSettings.songSort
+ MusicMode.ALBUMS -> musicSettings.albumSort
+ MusicMode.ARTISTS -> musicSettings.artistSort
+ MusicMode.GENRES -> musicSettings.genreSort
+ }
+
+ /**
+ * 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) {
+ logD("Updating ${_currentTabMode.value} sort to $sort")
+ // Can simply re-sort the current list of items without having to access the library.
+ when (_currentTabMode.value) {
+ MusicMode.SONGS -> {
+ musicSettings.songSort = sort
+ _songsList.value = sort.songs(_songsList.value)
}
- context.getString(R.string.set_key_hide_collaborators) -> {
- // Changes in the hide collaborator setting will change the artist contents
- // of the library, consider it a library update.
- onLibraryChanged(musicStore.library)
+ MusicMode.ALBUMS -> {
+ musicSettings.albumSort = sort
+ _albumsLists.value = sort.albums(_albumsLists.value)
+ }
+ MusicMode.ARTISTS -> {
+ musicSettings.artistSort = sort
+ _artistsList.value = sort.artists(_artistsList.value)
+ }
+ MusicMode.GENRES -> {
+ musicSettings.genreSort = sort
+ _genresList.value = sort.genres(_genresList.value)
}
}
}
@@ -155,46 +190,6 @@ class HomeViewModel(application: Application) :
_shouldRecreate.value = false
}
- /**
- * Get the preferred [Sort] for a given [Tab].
- * @param tabMode The [MusicMode] of the [Tab] desired.
- * @return The [Sort] preferred for that [Tab]
- */
- fun getSortForTab(tabMode: MusicMode) =
- when (tabMode) {
- MusicMode.SONGS -> settings.libSongSort
- MusicMode.ALBUMS -> settings.libAlbumSort
- MusicMode.ARTISTS -> settings.libArtistSort
- MusicMode.GENRES -> settings.libGenreSort
- }
-
- /**
- * 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) {
- logD("Updating ${_currentTabMode.value} sort to $sort")
- // Can simply re-sort the current list of items without having to access the library.
- when (_currentTabMode.value) {
- MusicMode.SONGS -> {
- settings.libSongSort = sort
- _songsList.value = sort.songs(_songsList.value)
- }
- MusicMode.ALBUMS -> {
- settings.libAlbumSort = sort
- _albumsLists.value = sort.albums(_albumsLists.value)
- }
- MusicMode.ARTISTS -> {
- settings.libArtistSort = sort
- _artistsList.value = sort.artists(_artistsList.value)
- }
- MusicMode.GENRES -> {
- settings.libGenreSort = sort
- _genresList.value = sort.genres(_genresList.value)
- }
- }
- }
-
/**
* 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.
@@ -209,5 +204,6 @@ class HomeViewModel(application: Application) :
* @return A list of the [MusicMode]s for each visible [Tab] in the configuration, ordered in
* the same way as the configuration.
*/
- private fun makeTabModes() = settings.libTabs.filterIsInstance().map { it.mode }
+ private fun makeTabModes() =
+ homeSettings.homeTabs.filterIsInstance().map { it.mode }
}
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 ad91fdade..e56a9c39e 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
@@ -30,14 +30,12 @@ import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment
+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.recycler.SelectionIndicatorAdapter
-import org.oxycblt.auxio.list.recycler.SyncListDiffer
-import org.oxycblt.auxio.music.Album
-import org.oxycblt.auxio.music.Music
-import org.oxycblt.auxio.music.MusicMode
-import org.oxycblt.auxio.music.MusicParent
-import org.oxycblt.auxio.music.Sort
+import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.library.Sort
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.playback.secsToMs
import org.oxycblt.auxio.util.collectImmediately
@@ -47,7 +45,7 @@ import org.oxycblt.auxio.util.collectImmediately
* @author Alexander Capehart (OxygenCobalt)
*/
class AlbumListFragment :
- ListFragment(),
+ ListFragment(),
FastScrollRecyclerView.Listener,
FastScrollRecyclerView.PopupProvider {
private val homeModel: HomeViewModel by activityViewModels()
@@ -69,8 +67,8 @@ class AlbumListFragment :
listener = this@AlbumListFragment
}
- collectImmediately(homeModel.albumsList, albumAdapter::replaceList)
- collectImmediately(selectionModel.selected, albumAdapter::setSelectedItems)
+ collectImmediately(homeModel.albumsList, ::updateList)
+ collectImmediately(selectionModel.selected, ::updateSelection)
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
@@ -125,45 +123,40 @@ class AlbumListFragment :
homeModel.setFastScrolling(isFastScrolling)
}
- override fun onRealClick(music: Music) {
- check(music is Album) { "Unexpected datatype: ${music::class.java}" }
- navModel.exploreNavigateTo(music)
+ override fun onRealClick(item: Album) {
+ navModel.exploreNavigateTo(item)
}
- override fun onOpenMenu(item: Item, anchor: View) {
- check(item is Album) { "Unexpected datatype: ${item::class.java}" }
+ override fun onOpenMenu(item: Album, anchor: View) {
openMusicMenu(anchor, R.menu.menu_album_actions, item)
}
+ private fun updateList(albums: List) {
+ albumAdapter.submitList(albums, BasicListInstructions.REPLACE)
+ }
+
+ private fun updateSelection(selection: List) {
+ albumAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
+ }
+
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
// If an album is playing, highlight it within this adapter.
- albumAdapter.setPlayingItem(parent as? Album, isPlaying)
+ albumAdapter.setPlaying(parent as? Album, isPlaying)
}
/**
* 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() {
- private val differ = SyncListDiffer(this, AlbumViewHolder.DIFF_CALLBACK)
-
- override val currentList: List-
- get() = differ.currentList
+ private class AlbumAdapter(private val listener: SelectableListListener) :
+ SelectionIndicatorAdapter(
+ ListDiffer.Blocking(AlbumViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
AlbumViewHolder.from(parent)
override fun onBindViewHolder(holder: AlbumViewHolder, position: Int) {
- holder.bind(differ.currentList[position], listener)
- }
-
- /**
- * Asynchronously update the list with new [Album]s.
- * @param newList The new [Album]s for the adapter to display.
- */
- fun replaceList(newList: List) {
- differ.replaceList(newList)
+ holder.bind(getItem(position), listener)
}
}
}
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 2dd51edf6..a0a4aed08 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
@@ -28,14 +28,15 @@ import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment
+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.recycler.SelectionIndicatorAdapter
-import org.oxycblt.auxio.list.recycler.SyncListDiffer
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
-import org.oxycblt.auxio.music.Sort
+import org.oxycblt.auxio.music.library.Sort
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.nonZeroOrNull
@@ -45,11 +46,11 @@ import org.oxycblt.auxio.util.nonZeroOrNull
* @author Alexander Capehart (OxygenCobalt)
*/
class ArtistListFragment :
- ListFragment(),
+ ListFragment(),
FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.Listener {
private val homeModel: HomeViewModel by activityViewModels()
- private val homeAdapter = ArtistAdapter(this)
+ private val artistAdapter = ArtistAdapter(this)
override fun onCreateBinding(inflater: LayoutInflater) =
FragmentHomeListBinding.inflate(inflater)
@@ -59,13 +60,13 @@ class ArtistListFragment :
binding.homeRecycler.apply {
id = R.id.home_artist_recycler
- adapter = homeAdapter
+ adapter = artistAdapter
popupProvider = this@ArtistListFragment
listener = this@ArtistListFragment
}
- collectImmediately(homeModel.artistsList, homeAdapter::replaceList)
- collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
+ collectImmediately(homeModel.artistsList, ::updateList)
+ collectImmediately(selectionModel.selected, ::updateSelection)
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
@@ -100,45 +101,40 @@ class ArtistListFragment :
homeModel.setFastScrolling(isFastScrolling)
}
- override fun onRealClick(music: Music) {
- check(music is Artist) { "Unexpected datatype: ${music::class.java}" }
- navModel.exploreNavigateTo(music)
+ override fun onRealClick(item: Artist) {
+ navModel.exploreNavigateTo(item)
}
- override fun onOpenMenu(item: Item, anchor: View) {
- check(item is Artist) { "Unexpected datatype: ${item::class.java}" }
+ override fun onOpenMenu(item: Artist, anchor: View) {
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
}
+ private fun updateList(artists: List) {
+ artistAdapter.submitList(artists, BasicListInstructions.REPLACE)
+ }
+
+ private fun updateSelection(selection: List) {
+ artistAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
+ }
+
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
// If an artist is playing, highlight it within this adapter.
- homeAdapter.setPlayingItem(parent as? Artist, isPlaying)
+ artistAdapter.setPlaying(parent as? Artist, isPlaying)
}
/**
* 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() {
- private val differ = SyncListDiffer(this, ArtistViewHolder.DIFF_CALLBACK)
-
- override val currentList: List
-
- get() = differ.currentList
+ private class ArtistAdapter(private val listener: SelectableListListener) :
+ SelectionIndicatorAdapter(
+ ListDiffer.Blocking(ArtistViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ArtistViewHolder.from(parent)
override fun onBindViewHolder(holder: ArtistViewHolder, position: Int) {
- holder.bind(differ.currentList[position], listener)
- }
-
- /**
- * Asynchronously update the list with new [Artist]s.
- * @param newList The new [Artist]s for the adapter to display.
- */
- fun replaceList(newList: List) {
- differ.replaceList(newList)
+ holder.bind(getItem(position), listener)
}
}
}
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 fa3484d90..83b49723d 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
@@ -28,14 +28,15 @@ import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment
+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.recycler.SelectionIndicatorAdapter
-import org.oxycblt.auxio.list.recycler.SyncListDiffer
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
-import org.oxycblt.auxio.music.Sort
+import org.oxycblt.auxio.music.library.Sort
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.collectImmediately
@@ -44,11 +45,11 @@ import org.oxycblt.auxio.util.collectImmediately
* @author Alexander Capehart (OxygenCobalt)
*/
class GenreListFragment :
- ListFragment(),
+ ListFragment(),
FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.Listener {
private val homeModel: HomeViewModel by activityViewModels()
- private val homeAdapter = GenreAdapter(this)
+ private val genreAdapter = GenreAdapter(this)
override fun onCreateBinding(inflater: LayoutInflater) =
FragmentHomeListBinding.inflate(inflater)
@@ -58,13 +59,13 @@ class GenreListFragment :
binding.homeRecycler.apply {
id = R.id.home_genre_recycler
- adapter = homeAdapter
+ adapter = genreAdapter
popupProvider = this@GenreListFragment
listener = this@GenreListFragment
}
- collectImmediately(homeModel.genresList, homeAdapter::replaceList)
- collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
+ collectImmediately(homeModel.genresList, ::updateList)
+ collectImmediately(selectionModel.selected, ::updateSelection)
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
@@ -99,45 +100,39 @@ class GenreListFragment :
homeModel.setFastScrolling(isFastScrolling)
}
- override fun onRealClick(music: Music) {
- check(music is Genre) { "Unexpected datatype: ${music::class.java}" }
- navModel.exploreNavigateTo(music)
+ override fun onRealClick(item: Genre) {
+ navModel.exploreNavigateTo(item)
}
- override fun onOpenMenu(item: Item, anchor: View) {
- check(item is Genre) { "Unexpected datatype: ${item::class.java}" }
+ override fun onOpenMenu(item: Genre, anchor: View) {
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
}
+ private fun updateList(artists: List) {
+ genreAdapter.submitList(artists, BasicListInstructions.REPLACE)
+ }
+
+ private fun updateSelection(selection: List) {
+ genreAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
+ }
+
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
// If a genre is playing, highlight it within this adapter.
- homeAdapter.setPlayingItem(parent as? Genre, isPlaying)
+ genreAdapter.setPlaying(parent as? Genre, isPlaying)
}
/**
* 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() {
- private val differ = SyncListDiffer(this, GenreViewHolder.DIFF_CALLBACK)
-
- override val currentList: List
-
- get() = differ.currentList
-
+ private class GenreAdapter(private val listener: SelectableListListener) :
+ SelectionIndicatorAdapter(
+ ListDiffer.Blocking(GenreViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
GenreViewHolder.from(parent)
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
- holder.bind(differ.currentList[position], listener)
- }
-
- /**
- * Asynchronously update the list with new [Genre]s.
- * @param newList The new [Genre]s for the adapter to display.
- */
- fun replaceList(newList: List) {
- differ.replaceList(newList)
+ holder.bind(getItem(position), listener)
}
}
}
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 0b85bcb5f..eba715958 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
@@ -30,17 +30,17 @@ import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.ListFragment
-import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
+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.recycler.SyncListDiffer
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
-import org.oxycblt.auxio.music.Sort
+import org.oxycblt.auxio.music.library.Sort
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.playback.secsToMs
-import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.collectImmediately
/**
@@ -48,11 +48,11 @@ import org.oxycblt.auxio.util.collectImmediately
* @author Alexander Capehart (OxygenCobalt)
*/
class SongListFragment :
- ListFragment(),
+ ListFragment(),
FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.Listener {
private val homeModel: HomeViewModel by activityViewModels()
- private val homeAdapter = SongAdapter(this)
+ private val songAdapter = SongAdapter(this)
// Save memory by re-using the same formatter and string builder when creating popup text
private val formatterSb = StringBuilder(64)
private val formatter = Formatter(formatterSb)
@@ -65,13 +65,13 @@ class SongListFragment :
binding.homeRecycler.apply {
id = R.id.home_song_recycler
- adapter = homeAdapter
+ adapter = songAdapter
popupProvider = this@SongListFragment
listener = this@SongListFragment
}
- collectImmediately(homeModel.songLists, homeAdapter::replaceList)
- collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
+ collectImmediately(homeModel.songsList, ::updateList)
+ collectImmediately(selectionModel.selected, ::updateSelection)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
@@ -86,7 +86,7 @@ class SongListFragment :
}
override fun getPopup(pos: Int): String? {
- val song = homeModel.songLists.value[pos]
+ val song = homeModel.songsList.value[pos]
// Change how we display the popup depending on the current sort mode.
// Note: We don't use the more correct individual artist name here, as sorts are largely
// based off the names of the parent objects and not the child objects.
@@ -130,27 +130,28 @@ class SongListFragment :
homeModel.setFastScrolling(isFastScrolling)
}
- override fun onRealClick(music: Music) {
- check(music is Song) { "Unexpected datatype: ${music::class.java}" }
- when (Settings(requireContext()).libPlaybackMode) {
- MusicMode.SONGS -> playbackModel.playFromAll(music)
- MusicMode.ALBUMS -> playbackModel.playFromAlbum(music)
- MusicMode.ARTISTS -> playbackModel.playFromArtist(music)
- MusicMode.GENRES -> playbackModel.playFromGenre(music)
- }
+ override fun onRealClick(item: Song) {
+ playbackModel.playFrom(item, homeModel.playbackMode)
}
- override fun onOpenMenu(item: Item, anchor: View) {
- check(item is Song) { "Unexpected datatype: ${item::class.java}" }
+ override fun onOpenMenu(item: Song, anchor: View) {
openMusicMenu(anchor, R.menu.menu_song_actions, item)
}
+ private fun updateList(songs: List) {
+ songAdapter.submitList(songs, BasicListInstructions.REPLACE)
+ }
+
+ private fun updateSelection(selection: List) {
+ songAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
+ }
+
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
if (parent == null) {
- homeAdapter.setPlayingItem(song, isPlaying)
+ songAdapter.setPlaying(song, isPlaying)
} else {
// Ignore playback that is not from all songs
- homeAdapter.setPlayingItem(null, isPlaying)
+ songAdapter.setPlaying(null, isPlaying)
}
}
@@ -158,26 +159,15 @@ 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() {
- private val differ = SyncListDiffer(this, SongViewHolder.DIFF_CALLBACK)
-
- override val currentList: List
-
- get() = differ.currentList
+ private class SongAdapter(private val listener: SelectableListListener) :
+ SelectionIndicatorAdapter(
+ ListDiffer.Blocking(SongViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
SongViewHolder.from(parent)
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
- holder.bind(differ.currentList[position], listener)
- }
-
- /**
- * Asynchronously update the list with new [Song]s.
- * @param newList The new [Song]s for the adapter to display.
- */
- fun replaceList(newList: List) {
- differ.replaceList(newList)
+ holder.bind(getItem(position), listener)
}
}
}
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 1e74ad396..76f7cf95d 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
@@ -17,7 +17,6 @@
package org.oxycblt.auxio.home.tabs
-import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.util.logE
@@ -26,7 +25,7 @@ import org.oxycblt.auxio.util.logE
* @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) : Item {
+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.
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 756f8fea1..c60084cf9 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
@@ -32,7 +32,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) :
+class TabAdapter(private val listener: EditableListListener) :
RecyclerView.Adapter() {
/** The current array of [Tab]s. */
var tabs = arrayOf()
@@ -93,7 +93,7 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) :
* @param listener A [EditableListListener] to bind interactions to.
*/
@SuppressLint("ClickableViewAccessibility")
- fun bind(tab: Tab, listener: EditableListListener) {
+ fun bind(tab: Tab, listener: EditableListListener) {
listener.bind(tab, this, dragHandle = binding.tabDragHandle)
binding.tabCheckBox.apply {
// Update the CheckBox name to align with the mode
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 6c9706762..e514413a4 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
@@ -25,9 +25,8 @@ import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogTabsBinding
+import org.oxycblt.auxio.home.HomeSettings
import org.oxycblt.auxio.list.EditableListListener
-import org.oxycblt.auxio.list.Item
-import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.logD
@@ -35,7 +34,8 @@ import org.oxycblt.auxio.util.logD
* A [ViewBindingDialogFragment] that allows the user to modify the home [Tab] configuration.
* @author Alexander Capehart (OxygenCobalt)
*/
-class TabCustomizeDialog : ViewBindingDialogFragment(), EditableListListener {
+class TabCustomizeDialog :
+ ViewBindingDialogFragment(), EditableListListener {
private val tabAdapter = TabAdapter(this)
private var touchHelper: ItemTouchHelper? = null
@@ -46,13 +46,13 @@ class TabCustomizeDialog : ViewBindingDialogFragment(), Edita
.setTitle(R.string.set_lib_tabs)
.setPositiveButton(R.string.lbl_ok) { _, _ ->
logD("Committing tab changes")
- Settings(requireContext()).libTabs = tabAdapter.tabs
+ HomeSettings.from(requireContext()).homeTabs = tabAdapter.tabs
}
.setNegativeButton(R.string.lbl_cancel, null)
}
override fun onBindingCreated(binding: DialogTabsBinding, savedInstanceState: Bundle?) {
- var tabs = Settings(requireContext()).libTabs
+ var tabs = HomeSettings.from(requireContext()).homeTabs
// Try to restore a pending tab configuration that was saved prior.
if (savedInstanceState != null) {
val savedTabs = Tab.fromIntCode(savedInstanceState.getInt(KEY_TABS))
@@ -81,8 +81,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment(), Edita
binding.tabRecycler.adapter = null
}
- override fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) {
- check(item is Tab) { "Unexpected datatype: ${item::class.java}" }
+ override fun onClick(item: Tab, viewHolder: RecyclerView.ViewHolder) {
// We will need the exact index of the tab to update on in order to
// notify the adapter of the change.
val index = tabAdapter.tabs.indexOfFirst { it.mode == item.mode }
diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt
new file mode 100644
index 000000000..d5dd32dd1
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.image
+
+import android.content.Context
+import androidx.core.content.edit
+import org.oxycblt.auxio.R
+import org.oxycblt.auxio.settings.Settings
+import org.oxycblt.auxio.util.logD
+
+/**
+ * User configuration specific to image loading.
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+interface ImageSettings : Settings {
+ /** The strategy to use when loading album covers. */
+ val coverMode: CoverMode
+
+ interface Listener {
+ /** Called when [coverMode] changes. */
+ fun onCoverModeChanged() {}
+ }
+
+ private class Real(context: Context) : Settings.Real(context), ImageSettings {
+ override val coverMode: CoverMode
+ get() =
+ CoverMode.fromIntCode(
+ sharedPreferences.getInt(getString(R.string.set_key_cover_mode), Int.MIN_VALUE))
+ ?: CoverMode.MEDIA_STORE
+
+ override fun migrate() {
+ // Show album covers and Ignore MediaStore covers were unified in 3.0.0
+ if (sharedPreferences.contains(OLD_KEY_SHOW_COVERS) ||
+ sharedPreferences.contains(OLD_KEY_QUALITY_COVERS)) {
+ logD("Migrating cover settings")
+
+ val mode =
+ when {
+ !sharedPreferences.getBoolean(OLD_KEY_SHOW_COVERS, true) -> CoverMode.OFF
+ !sharedPreferences.getBoolean(OLD_KEY_QUALITY_COVERS, true) ->
+ CoverMode.MEDIA_STORE
+ else -> CoverMode.QUALITY
+ }
+
+ sharedPreferences.edit {
+ putInt(getString(R.string.set_key_cover_mode), mode.intCode)
+ remove(OLD_KEY_SHOW_COVERS)
+ remove(OLD_KEY_QUALITY_COVERS)
+ }
+ }
+ }
+
+ override fun onSettingChanged(key: String, listener: Listener) {
+ if (key == getString(R.string.set_key_cover_mode)) {
+ listOf(key, listener)
+ }
+ }
+
+ private companion object {
+ const val OLD_KEY_SHOW_COVERS = "KEY_SHOW_COVERS"
+ const val OLD_KEY_QUALITY_COVERS = "KEY_QUALITY_COVERS"
+ }
+ }
+
+ companion object {
+ /**
+ * Get a framework-backed implementation.
+ * @param context [Context] required.
+ */
+ fun from(context: Context): ImageSettings = Real(context)
+ }
+}
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 f5df8f9bb..5da781bb3 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt
@@ -28,7 +28,7 @@ import androidx.core.widget.ImageViewCompat
import com.google.android.material.shape.MaterialShapeDrawable
import kotlin.math.max
import org.oxycblt.auxio.R
-import org.oxycblt.auxio.settings.Settings
+import org.oxycblt.auxio.ui.UISettings
import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.getDrawableCompat
@@ -52,7 +52,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
private val indicatorMatrix = Matrix()
private val indicatorMatrixSrc = RectF()
private val indicatorMatrixDst = RectF()
- private val settings = Settings(context)
/**
* The corner radius of this view. This allows the outer ImageGroup to apply it's corner radius
@@ -62,7 +61,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
set(value) {
field = value
(background as? MaterialShapeDrawable)?.let { bg ->
- if (settings.roundMode) {
+ if (UISettings.from(context).roundMode) {
bg.setCornerSize(value)
} else {
bg.setCornerSize(0f)
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 590404a6a..d838a7b63 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt
@@ -39,7 +39,7 @@ 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.settings.Settings
+import org.oxycblt.auxio.ui.UISettings
import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.getDrawableCompat
@@ -81,7 +81,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
background =
MaterialShapeDrawable().apply {
fillColor = context.getColorCompat(R.color.sel_cover_bg)
- if (Settings(context).roundMode) {
+ if (UISettings.from(context).roundMode) {
// Only use the specified corner radius when round mode is enabled.
setCornerSize(cornerRadius)
}
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 8c9acd412..a324690a2 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
@@ -35,7 +35,7 @@ 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.music.Sort
+import org.oxycblt.auxio.music.library.Sort
/**
* A [Keyer] implementation for [Music] data.
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
index 31a4bda55..b26141f7b 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Covers.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Covers.kt
@@ -29,8 +29,8 @@ 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.settings.Settings
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
@@ -47,10 +47,8 @@ object Covers {
* loading failed or should not occur.
*/
suspend fun fetch(context: Context, album: Album): InputStream? {
- val settings = Settings(context)
-
return try {
- when (settings.coverMode) {
+ when (ImageSettings.from(context).coverMode) {
CoverMode.OFF -> null
CoverMode.MEDIA_STORE -> fetchMediaStoreCovers(context, album)
CoverMode.QUALITY -> fetchQualityCovers(context, album)
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 250992d04..dee519263 100644
--- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt
@@ -37,7 +37,8 @@ import org.oxycblt.auxio.util.showToast
* A Fragment containing a selectable list.
* @author Alexander Capehart (OxygenCobalt)
*/
-abstract class ListFragment : SelectionFragment(), SelectableListListener {
+abstract class ListFragment :
+ SelectionFragment(), SelectableListListener {
protected val navModel: NavigationViewModel by activityViewModels()
private var currentMenu: PopupMenu? = null
@@ -50,12 +51,11 @@ abstract class ListFragment : SelectionFragment(), Selecta
/**
* 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 music The [Music] item that was clicked.
+ * @param item The [T] data of the item that was clicked.
*/
- abstract fun onRealClick(music: Music)
+ abstract fun onRealClick(item: T)
- override fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) {
- check(item is Music) { "Unexpected datatype: ${item::class.simpleName}" }
+ override fun onClick(item: T, viewHolder: RecyclerView.ViewHolder) {
if (selectionModel.selected.value.isNotEmpty()) {
// Map clicking an item to selecting an item when items are already selected.
selectionModel.select(item)
@@ -65,8 +65,7 @@ abstract class ListFragment : SelectionFragment(), Selecta
}
}
- override fun onSelect(item: Item) {
- check(item is Music) { "Unexpected datatype: ${item::class.simpleName}" }
+ override fun onSelect(item: T) {
selectionModel.select(item)
}
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 c8f9ebb6d..1afa340c3 100644
--- a/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt
+++ b/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt
@@ -25,26 +25,22 @@ import androidx.recyclerview.widget.RecyclerView
* A basic listener for list interactions.
* @author Alexander Capehart (OxygenCobalt)
*/
-interface ClickableListListener {
+interface ClickableListListener {
/**
- * Called when an [Item] in the list is clicked.
- * @param item The [Item] that was clicked.
+ * 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.
*/
- fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder)
+ fun onClick(item: T, viewHolder: RecyclerView.ViewHolder)
/**
* Binds this instance to a list item.
- * @param item The [Item] that this list entry is bound to.
+ * @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.
*/
- fun bind(
- item: Item,
- viewHolder: RecyclerView.ViewHolder,
- bodyView: View = viewHolder.itemView
- ) {
+ fun bind(item: T, viewHolder: RecyclerView.ViewHolder, bodyView: View = viewHolder.itemView) {
bodyView.setOnClickListener { onClick(item, viewHolder) }
}
}
@@ -53,7 +49,7 @@ interface ClickableListListener {
* An extension of [ClickableListListener] that enables list editing functionality.
* @author Alexander Capehart (OxygenCobalt)
*/
-interface EditableListListener : ClickableListListener {
+interface EditableListListener : ClickableListListener {
/**
* Called when a [RecyclerView.ViewHolder] requests that it should be dragged.
* @param viewHolder The [RecyclerView.ViewHolder] that should start being dragged.
@@ -62,14 +58,14 @@ interface EditableListListener : ClickableListListener {
/**
* Binds this instance to a list item.
- * @param item The [Item] that this list entry is bound to.
+ * @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.
* @param dragHandle A touchable [View]. Any drag on this view will start a drag event.
*/
fun bind(
- item: Item,
+ item: T,
viewHolder: RecyclerView.ViewHolder,
bodyView: View = viewHolder.itemView,
dragHandle: View
@@ -89,30 +85,30 @@ interface EditableListListener : ClickableListListener {
* An extension of [ClickableListListener] that enables menu and selection functionality.
* @author Alexander Capehart (OxygenCobalt)
*/
-interface SelectableListListener : ClickableListListener {
+interface SelectableListListener : ClickableListListener {
/**
- * Called when an [Item] in the list requests that a menu related to it should be opened.
- * @param item The [Item] to show a menu for.
+ * Called when an item in the list requests that a menu related to it should be opened.
+ * @param item The [T] item to open a menu for.
* @param anchor The [View] to anchor the menu to.
*/
- fun onOpenMenu(item: Item, anchor: View)
+ fun onOpenMenu(item: T, anchor: View)
/**
- * Called when an [Item] in the list requests that it be selected.
- * @param item The [Item] to select.
+ * Called when an item in the list requests that it be selected.
+ * @param item The [T] item to select.
*/
- fun onSelect(item: Item)
+ fun onSelect(item: T)
/**
* Binds this instance to a list item.
- * @param item The [Item] that this list entry is bound to.
+ * @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.
* @param menuButton A clickable [View]. Any click events on this [View] will open a menu.
*/
fun bind(
- item: Item,
+ item: T,
viewHolder: RecyclerView.ViewHolder,
bodyView: View = viewHolder.itemView,
menuButton: View
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
new file mode 100644
index 000000000..23e49344d
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/DiffAdapter.kt
@@ -0,0 +1,53 @@
+/*
+ * 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/ListDiffer.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/ListDiffer.kt
new file mode 100644
index 000000000..e2be45833
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/ListDiffer.kt
@@ -0,0 +1,226 @@
+/*
+ * 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 =
+ RealAsyncListDiffer(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 =
+ RealBlockingListDiffer(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 RealAsyncListDiffer(
+ 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 RealBlockingListDiffer(
+ 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/recycler/PlayingIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt
similarity index 87%
rename from app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt
rename to app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt
index ceb1fbf0b..588c5c9b6 100644
--- a/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt
@@ -15,32 +15,27 @@
* along with this program. If not, see .
*/
-package org.oxycblt.auxio.list.recycler
+package org.oxycblt.auxio.list.adapter
import android.view.View
import androidx.recyclerview.widget.RecyclerView
-import org.oxycblt.auxio.list.Item
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.
* @author Alexander Capehart (OxygenCobalt)
*/
-abstract class PlayingIndicatorAdapter : RecyclerView.Adapter() {
+abstract class PlayingIndicatorAdapter(
+ differFactory: ListDiffer.Factory
+) : DiffAdapter(differFactory) {
// 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
// marked as "playing" or not.
- private var currentItem: Item? = null
+ private var currentItem: T? = null
private var isPlaying = false
- /**
- * The current list of the adapter. This is used to update items if the indicator state changes.
- */
- abstract val currentList: List
-
-
- override fun getItemCount() = currentList.size
-
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) {
@@ -55,10 +50,10 @@ abstract class PlayingIndicatorAdapter : RecyclerV
}
/**
* Update the currently playing item in the list.
- * @param item The item currently being played, or null if it is not being played.
+ * @param item The [T] currently being played, or null if it is not being played.
* @param isPlaying Whether playback is ongoing or paused.
*/
- fun setPlayingItem(item: Item?, isPlaying: Boolean) {
+ fun setPlaying(item: T?, isPlaying: Boolean) {
var updatedItem = false
if (currentItem != item) {
val oldItem = currentItem
diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt
similarity index 86%
rename from app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt
rename to app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt
index 64036c7cd..20239546c 100644
--- a/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package org.oxycblt.auxio.list.recycler
+package org.oxycblt.auxio.list.adapter
import android.view.View
import androidx.recyclerview.widget.RecyclerView
@@ -24,11 +24,13 @@ 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.
* @author Alexander Capehart (OxygenCobalt)
*/
-abstract class SelectionIndicatorAdapter :
- PlayingIndicatorAdapter() {
- private var selectedItems = setOf()
+abstract class SelectionIndicatorAdapter(
+ differFactory: ListDiffer.Factory
+) : PlayingIndicatorAdapter(differFactory) {
+ private var selectedItems = setOf()
override fun onBindViewHolder(holder: VH, position: Int, payloads: List) {
super.onBindViewHolder(holder, position, payloads)
@@ -39,9 +41,9 @@ abstract class SelectionIndicatorAdapter :
/**
* Update the list of selected items.
- * @param items A list of selected [Music].
+ * @param items A set of selected [T] items.
*/
- fun setSelectedItems(items: List) {
+ fun setSelected(items: Set) {
val oldSelectedItems = selectedItems
val newSelectedItems = items.toSet()
if (newSelectedItems == oldSelectedItems) {
diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/SimpleItemCallback.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/SimpleDiffCallback.kt
similarity index 91%
rename from app/src/main/java/org/oxycblt/auxio/list/recycler/SimpleItemCallback.kt
rename to app/src/main/java/org/oxycblt/auxio/list/adapter/SimpleDiffCallback.kt
index 9a289fc88..358c3b3f1 100644
--- a/app/src/main/java/org/oxycblt/auxio/list/recycler/SimpleItemCallback.kt
+++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/SimpleDiffCallback.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package org.oxycblt.auxio.list.recycler
+package org.oxycblt.auxio.list.adapter
import androidx.recyclerview.widget.DiffUtil
import org.oxycblt.auxio.list.Item
@@ -25,6 +25,6 @@ import org.oxycblt.auxio.list.Item
* whenever creating [DiffUtil.ItemCallback] implementations with an [Item] subclass.
* @author Alexander Capehart (OxygenCobalt)
*/
-abstract class SimpleItemCallback : DiffUtil.ItemCallback() {
+abstract class SimpleDiffCallback : DiffUtil.ItemCallback() {
final override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem == newItem
}
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 b29e8cb7f..06eea4411 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
@@ -45,6 +45,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
// Auxio's non-dialog RecyclerViews never change their size based on adapter contents,
// so we can enable fixed-size optimizations.
setHasFixedSize(true)
+ addItemDecoration(HeaderItemDecoration(context))
}
final override fun setHasFixedSize(hasFixedSize: Boolean) {
@@ -52,6 +53,10 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
super.setHasFixedSize(hasFixedSize)
}
+ final override fun addItemDecoration(decor: ItemDecoration) {
+ super.addItemDecoration(decor)
+ }
+
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
// Update the RecyclerView's padding such that the bottom insets are applied
// while still preserving bottom padding.
@@ -78,6 +83,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
}
}
+ /** A [RecyclerView.Adapter]-specific hook to control divider decoration visibility. */
+
/** An [RecyclerView.Adapter]-specific hook to [GridLayoutManager.SpanSizeLookup]. */
interface SpanSizeLookup {
/**
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
new file mode 100644
index 000000000..b715b8b70
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/HeaderItemDecoration.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.recycler
+
+import android.content.Context
+import android.util.AttributeSet
+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
+
+/**
+ * A [BackportMaterialDividerItemDecoration] that sets up the divider configuration to correctly
+ * separate content with headers.
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+class HeaderItemDecoration
+@JvmOverloads
+constructor(
+ context: Context,
+ attributeSet: AttributeSet? = null,
+ defStyleAttr: Int = R.attr.materialDividerStyle,
+ orientation: Int = LinearLayoutManager.VERTICAL
+) : BackportMaterialDividerItemDecoration(context, attributeSet, defStyleAttr, orientation) {
+ override fun shouldDrawDivider(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
+ } catch (e: ClassCastException) {
+ false
+ } catch (e: IndexOutOfBoundsException) {
+ false
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/SyncListDiffer.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/SyncListDiffer.kt
deleted file mode 100644
index 4b47c22c3..000000000
--- a/app/src/main/java/org/oxycblt/auxio/list/recycler/SyncListDiffer.kt
+++ /dev/null
@@ -1,141 +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.list.recycler
-
-import androidx.recyclerview.widget.AdapterListUpdateCallback
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.RecyclerView
-
-/**
- * A list differ that operates synchronously. This can help resolve some shortcomings with
- * AsyncListDiffer, at the cost of performance. Derived from Material Files:
- * https://github.com/zhanghai/MaterialFiles
- * @author Hai Zhang, Alexander Capehart (OxygenCobalt)
- */
-class SyncListDiffer(
- adapter: RecyclerView.Adapter<*>,
- private val diffCallback: DiffUtil.ItemCallback
-) {
- private val updateCallback = AdapterListUpdateCallback(adapter)
-
- var currentList: List = emptyList()
- private set(newList) {
- if (newList === currentList || newList.isEmpty() && currentList.isEmpty()) {
- return
- }
-
- if (newList.isEmpty()) {
- val oldListSize = currentList.size
- field = emptyList()
- updateCallback.onRemoved(0, oldListSize)
- return
- }
-
- if (currentList.isEmpty()) {
- field = newList
- updateCallback.onInserted(0, newList.size)
- 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()
- }
- }
- })
-
- field = newList
- result.dispatchUpdatesTo(updateCallback)
- }
-
- /**
- * Submit a list like AsyncListDiffer. This is exceedingly slow for large diffs, so only use it
- * if the changes are trivial.
- * @param newList The list to update to.
- */
- fun submitList(newList: List) {
- if (newList == currentList) {
- // Nothing to do.
- return
- }
-
- currentList = newList
- }
-
- /**
- * Replace this list with a new list. This is good for large diffs that are too slow to update
- * synchronously, but too chaotic to update asynchronously.
- * @param newList The list to update to.
- */
- fun replaceList(newList: List) {
- if (newList == currentList) {
- // Nothing to do.
- return
- }
-
- currentList = emptyList()
- currentList = newList
- }
-}
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 15628c41c..d72bf6814 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
@@ -26,13 +26,13 @@ import org.oxycblt.auxio.databinding.ItemParentBinding
import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.SelectableListListener
-import org.oxycblt.auxio.music.Album
-import org.oxycblt.auxio.music.Artist
-import org.oxycblt.auxio.music.Genre
-import org.oxycblt.auxio.music.Song
+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
+import org.oxycblt.auxio.util.logD
/**
* A [RecyclerView.ViewHolder] that displays a [Song]. Use [from] to create an instance.
@@ -45,7 +45,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
* @param song The new [Song] to bind.
* @param listener An [SelectableListListener] to bind interactions to.
*/
- fun bind(song: Song, listener: SelectableListListener) {
+ fun bind(song: Song, listener: SelectableListListener) {
listener.bind(song, this, menuButton = binding.songMenu)
binding.songAlbumCover.bind(song)
binding.songName.text = song.resolveName(binding.context)
@@ -74,7 +74,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
- object : SimpleItemCallback() {
+ object : SimpleDiffCallback() {
override fun areContentsTheSame(oldItem: Song, newItem: Song) =
oldItem.rawName == newItem.rawName && oldItem.areArtistContentsTheSame(newItem)
}
@@ -92,7 +92,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
* @param album The new [Album] to bind.
* @param listener An [SelectableListListener] to bind interactions to.
*/
- fun bind(album: Album, listener: SelectableListListener) {
+ fun bind(album: Album, listener: SelectableListListener) {
listener.bind(album, this, menuButton = binding.parentMenu)
binding.parentImage.bind(album)
binding.parentName.text = album.resolveName(binding.context)
@@ -121,11 +121,11 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
- object : SimpleItemCallback() {
+ object : SimpleDiffCallback() {
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
oldItem.rawName == newItem.rawName &&
oldItem.areArtistContentsTheSame(newItem) &&
- oldItem.type == newItem.type
+ oldItem.releaseType == newItem.releaseType
}
}
}
@@ -141,7 +141,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
* @param artist The new [Artist] to bind.
* @param listener An [SelectableListListener] to bind interactions to.
*/
- fun bind(artist: Artist, listener: SelectableListListener) {
+ fun bind(artist: Artist, listener: SelectableListListener) {
listener.bind(artist, this, menuButton = binding.parentMenu)
binding.parentImage.bind(artist)
binding.parentName.text = artist.resolveName(binding.context)
@@ -180,7 +180,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
- object : SimpleItemCallback() {
+ object : SimpleDiffCallback() {
override fun areContentsTheSame(oldItem: Artist, newItem: Artist) =
oldItem.rawName == newItem.rawName &&
oldItem.albums.size == newItem.albums.size &&
@@ -200,7 +200,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
* @param genre The new [Genre] to bind.
* @param listener An [SelectableListListener] to bind interactions to.
*/
- fun bind(genre: Genre, listener: SelectableListListener) {
+ fun bind(genre: Genre, listener: SelectableListListener) {
listener.bind(genre, this, menuButton = binding.parentMenu)
binding.parentImage.bind(genre)
binding.parentName.text = genre.resolveName(binding.context)
@@ -233,7 +233,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
- object : SimpleItemCallback() {
+ object : SimpleDiffCallback() {
override fun areContentsTheSame(oldItem: Genre, newItem: Genre): Boolean =
oldItem.rawName == newItem.rawName && oldItem.songs.size == newItem.songs.size
}
@@ -251,6 +251,7 @@ class HeaderViewHolder private constructor(private val binding: ItemHeaderBindin
* @param header The new [Header] to bind.
*/
fun bind(header: Header) {
+ logD(binding.context.getString(header.titleRes))
binding.title.text = binding.context.getString(header.titleRes)
}
@@ -268,7 +269,7 @@ class HeaderViewHolder private constructor(private val binding: ItemHeaderBindin
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
- object : SimpleItemCallback() {
+ object : SimpleDiffCallback() {
override fun areContentsTheSame(oldItem: Header, newItem: Header): Boolean =
oldItem.titleRes == newItem.titleRes
}
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 a5d762c42..32edb8f7a 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
@@ -71,6 +71,14 @@ abstract class SelectionFragment :
requireContext().showToast(R.string.lng_queue_added)
true
}
+ R.id.action_selection_play -> {
+ playbackModel.play(selectionModel.consume())
+ true
+ }
+ R.id.action_selection_shuffle -> {
+ playbackModel.shuffle(selectionModel.consume())
+ true
+ }
else -> false
}
}
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 754e8ca08..a607b9cd6 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
@@ -21,6 +21,8 @@ import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.*
+import org.oxycblt.auxio.music.MusicStore
+import org.oxycblt.auxio.music.library.Library
/**
* A [ViewModel] that manages the current selection.
@@ -38,7 +40,7 @@ class SelectionViewModel : ViewModel(), MusicStore.Listener {
musicStore.addListener(this)
}
- override fun onLibraryChanged(library: MusicStore.Library?) {
+ override fun onLibraryChanged(library: Library?) {
if (library == null) {
return
}
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 095bae072..39aeab02d 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt
@@ -21,6 +21,7 @@ package org.oxycblt.auxio.music
import android.content.Context
import android.os.Parcelable
+import androidx.annotation.VisibleForTesting
import java.security.MessageDigest
import java.text.CollationKey
import java.text.Collator
@@ -30,10 +31,12 @@ import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Item
-import org.oxycblt.auxio.music.filesystem.*
+import org.oxycblt.auxio.music.library.Sort
import org.oxycblt.auxio.music.parsing.parseId3GenreNames
import org.oxycblt.auxio.music.parsing.parseMultiValue
-import org.oxycblt.auxio.settings.Settings
+import org.oxycblt.auxio.music.storage.*
+import org.oxycblt.auxio.music.tags.Date
+import org.oxycblt.auxio.music.tags.ReleaseType
import org.oxycblt.auxio.util.nonZeroOrNull
import org.oxycblt.auxio.util.unlikelyToBeNull
@@ -308,10 +311,10 @@ sealed class MusicParent : Music() {
/**
* A song. Perhaps the foundation of the entirety of Auxio.
* @param raw The [Song.Raw] to derive the member data from.
- * @param settings [Settings] to determine the artist configuration.
+ * @param musicSettings [MusicSettings] to perform further user-configured parsing.
* @author Alexander Capehart (OxygenCobalt)
*/
-class Song constructor(raw: Raw, settings: Settings) : Music() {
+class Song constructor(raw: Raw, musicSettings: MusicSettings) : Music() {
override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
raw.musicBrainzId?.toUuidOrNull()?.let { UID.musicBrainz(MusicMode.SONGS, it) }
@@ -381,9 +384,9 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
val album: Album
get() = unlikelyToBeNull(_album)
- private val artistMusicBrainzIds = raw.artistMusicBrainzIds.parseMultiValue(settings)
- private val artistNames = raw.artistNames.parseMultiValue(settings)
- private val artistSortNames = raw.artistSortNames.parseMultiValue(settings)
+ private val artistMusicBrainzIds = raw.artistMusicBrainzIds.parseMultiValue(musicSettings)
+ private val artistNames = raw.artistNames.parseMultiValue(musicSettings)
+ private val artistSortNames = raw.artistSortNames.parseMultiValue(musicSettings)
private val rawArtists =
artistNames.mapIndexed { i, name ->
Artist.Raw(
@@ -392,9 +395,10 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
artistSortNames.getOrNull(i))
}
- private val albumArtistMusicBrainzIds = raw.albumArtistMusicBrainzIds.parseMultiValue(settings)
- private val albumArtistNames = raw.albumArtistNames.parseMultiValue(settings)
- private val albumArtistSortNames = raw.albumArtistSortNames.parseMultiValue(settings)
+ private val albumArtistMusicBrainzIds =
+ raw.albumArtistMusicBrainzIds.parseMultiValue(musicSettings)
+ private val albumArtistNames = raw.albumArtistNames.parseMultiValue(musicSettings)
+ private val albumArtistSortNames = raw.albumArtistSortNames.parseMultiValue(musicSettings)
private val rawAlbumArtists =
albumArtistNames.mapIndexed { i, name ->
Artist.Raw(
@@ -462,7 +466,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
musicBrainzId = raw.albumMusicBrainzId?.toUuidOrNull(),
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
sortName = raw.albumSortName,
- type = Album.Type.parse(raw.albumTypes.parseMultiValue(settings)),
+ releaseType = ReleaseType.parse(raw.releaseTypes.parseMultiValue(musicSettings)),
rawArtists =
rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) })
@@ -481,7 +485,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
*/
val _rawGenres =
raw.genreNames
- .parseId3GenreNames(settings)
+ .parseId3GenreNames(musicSettings)
.map { Genre.Raw(it) }
.ifEmpty { listOf(Genre.Raw()) }
@@ -581,8 +585,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
var albumName: String? = null,
/** @see Album.Raw.sortName */
var albumSortName: String? = null,
- /** @see Album.Raw.type */
- var albumTypes: List = listOf(),
+ /** @see Album.Raw.releaseType */
+ var releaseTypes: List = listOf(),
/** @see Artist.Raw.musicBrainzId */
var artistMusicBrainzIds: List = listOf(),
/** @see Artist.Raw.name */
@@ -628,10 +632,10 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent(
val dates = Date.Range.from(songs.mapNotNull { it.date })
/**
- * The [Type] of this album, signifying the type of release it actually is. Defaults to
- * [Type.Album].
+ * The [ReleaseType] of this album, signifying the type of release it actually is. Defaults to
+ * [ReleaseType.Album].
*/
- val type = raw.type ?: Type.Album(null)
+ val releaseType = raw.releaseType ?: ReleaseType.Album(null)
/**
* The URI to a MediaStore-provided album cover. These images will be fast to load, but at the
* cost of image quality.
@@ -726,201 +730,6 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent(
}
}
- /**
- * The type of release an [Album] is considered. This includes EPs, Singles, Compilations, etc.
- *
- * This class is derived from the MusicBrainz Release Group Type specification. It can be found
- * at: https://musicbrainz.org/doc/Release_Group/Type
- * @author Alexander Capehart (OxygenCobalt)
- */
- sealed class Type {
- /**
- * A specification of what kind of performance this release is. If null, the release is
- * considered "Plain".
- */
- abstract val refinement: Refinement?
-
- /** The string resource corresponding to the name of this release type to show in the UI. */
- abstract val stringRes: Int
-
- /**
- * A plain album.
- * @param refinement A specification of what kind of performance this release is. If null,
- * the release is considered "Plain".
- */
- data class Album(override val refinement: Refinement?) : Type() {
- override val stringRes: Int
- get() =
- when (refinement) {
- null -> R.string.lbl_album
- // If present, include the refinement in the name of this release type.
- Refinement.LIVE -> R.string.lbl_album_live
- Refinement.REMIX -> R.string.lbl_album_remix
- }
- }
-
- /**
- * A "Extended Play", or EP. Usually a smaller release consisting of 4-5 songs.
- * @param refinement A specification of what kind of performance this release is. If null,
- * the release is considered "Plain".
- */
- data class EP(override val refinement: Refinement?) : Type() {
- override val stringRes: Int
- get() =
- when (refinement) {
- null -> R.string.lbl_ep
- // If present, include the refinement in the name of this release type.
- Refinement.LIVE -> R.string.lbl_ep_live
- Refinement.REMIX -> R.string.lbl_ep_remix
- }
- }
-
- /**
- * A single. Usually a release consisting of 1-2 songs.
- * @param refinement A specification of what kind of performance this release is. If null,
- * the release is considered "Plain".
- */
- data class Single(override val refinement: Refinement?) : Type() {
- override val stringRes: Int
- get() =
- when (refinement) {
- null -> R.string.lbl_single
- // If present, include the refinement in the name of this release type.
- Refinement.LIVE -> R.string.lbl_single_live
- Refinement.REMIX -> R.string.lbl_single_remix
- }
- }
-
- /**
- * A compilation. Usually consists of many songs from a variety of artists.
- * @param refinement A specification of what kind of performance this release is. If null,
- * the release is considered "Plain".
- */
- data class Compilation(override val refinement: Refinement?) : Type() {
- override val stringRes: Int
- get() =
- when (refinement) {
- null -> R.string.lbl_compilation
- // If present, include the refinement in the name of this release type.
- Refinement.LIVE -> R.string.lbl_compilation_live
- Refinement.REMIX -> R.string.lbl_compilation_remix
- }
- }
-
- /**
- * A soundtrack. Similar to a [Compilation], but created for a specific piece of (usually
- * visual) media.
- */
- object Soundtrack : Type() {
- override val refinement: Refinement?
- get() = null
-
- override val stringRes: Int
- get() = R.string.lbl_soundtrack
- }
-
- /**
- * A (DJ) Mix. These are usually one large track consisting of the artist playing several
- * sub-tracks with smooth transitions between them.
- */
- object Mix : Type() {
- override val refinement: Refinement?
- get() = null
-
- override val stringRes: Int
- get() = R.string.lbl_mix
- }
-
- /**
- * A Mix-tape. These are usually [EP]-sized releases of music made to promote an [Artist] or
- * a future release.
- */
- object Mixtape : Type() {
- override val refinement: Refinement?
- get() = null
-
- override val stringRes: Int
- get() = R.string.lbl_mixtape
- }
-
- /** A specification of what kind of performance a particular release is. */
- enum class Refinement {
- /** A release consisting of a live performance */
- LIVE,
-
- /** A release consisting of another [Artist]s remix of a prior performance. */
- REMIX
- }
-
- companion object {
- /**
- * Parse a [Type] from a string formatted with the MusicBrainz Release Group Type
- * specification.
- * @param types A list of values consisting of valid release type values.
- * @return A [Type] consisting of the given types, or null if the types were not valid.
- */
- fun parse(types: List): Type? {
- val primary = types.getOrNull(0) ?: return null
- return when {
- // Primary types should be the first types in the sequence.
- primary.equals("album", true) -> types.parseSecondaryTypes(1) { Album(it) }
- primary.equals("ep", true) -> types.parseSecondaryTypes(1) { EP(it) }
- primary.equals("single", true) -> types.parseSecondaryTypes(1) { Single(it) }
- // The spec makes no mention of whether primary types are a pre-requisite for
- // secondary types, so we assume that it's not and map oprhan secondary types
- // to Album release types.
- else -> types.parseSecondaryTypes(0) { Album(it) }
- }
- }
-
- /**
- * Parse "secondary" types (i.e not [Album], [EP], or [Single]) from a string formatted
- * with the MusicBrainz Release Group Type specification.
- * @param index The index of the release type to parse.
- * @param convertRefinement Code to convert a [Refinement] into a [Type] corresponding
- * to the callee's context. This is used in order to handle secondary times that are
- * actually [Refinement]s.
- * @return A [Type] corresponding to the secondary type found at that index.
- */
- private inline fun List.parseSecondaryTypes(
- index: Int,
- convertRefinement: (Refinement?) -> Type
- ): Type {
- val secondary = getOrNull(index)
- return if (secondary.equals("compilation", true)) {
- // Secondary type is a compilation, actually parse the third type
- // and put that into a compilation if needed.
- parseSecondaryTypeImpl(getOrNull(index + 1)) { Compilation(it) }
- } else {
- // Secondary type is a plain value, use the original values given.
- parseSecondaryTypeImpl(secondary, convertRefinement)
- }
- }
-
- /**
- * Parse "secondary" types (i.e not [Album], [EP], [Single]) that do not correspond to
- * any child values.
- * @param type The release type value to parse.
- * @param convertRefinement Code to convert a [Refinement] into a [Type] corresponding
- * to the callee's context. This is used in order to handle secondary times that are
- * actually [Refinement]s.
- */
- private inline fun parseSecondaryTypeImpl(
- type: String?,
- convertRefinement: (Refinement?) -> Type
- ) =
- when {
- // Parse all the types that have no children
- type.equals("soundtrack", true) -> Soundtrack
- type.equals("mixtape/street", true) -> Mixtape
- type.equals("dj-mix", true) -> Mix
- type.equals("live", true) -> convertRefinement(Refinement.LIVE)
- type.equals("remix", true) -> convertRefinement(Refinement.REMIX)
- else -> convertRefinement(null)
- }
- }
- }
-
/**
* Raw information about an [Album] obtained from the component [Song] instances. **This is only
* meant for use within the music package.**
@@ -937,8 +746,8 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent(
val name: String,
/** @see Music.rawSortName */
val sortName: String?,
- /** @see Album.type */
- val type: Type?,
+ /** @see Album.releaseType */
+ val releaseType: ReleaseType?,
/** @see Artist.Raw.name */
val rawArtists: List
) {
@@ -955,16 +764,15 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent(
override fun hashCode() = hashCode
- override fun equals(other: Any?): Boolean {
- if (other !is Raw) return false
- if (musicBrainzId != null &&
- other.musicBrainzId != null &&
- musicBrainzId == other.musicBrainzId) {
- return true
- }
-
- return name.equals(other.name, true) && rawArtists == other.rawArtists
- }
+ override fun equals(other: Any?) =
+ other is Raw &&
+ when {
+ musicBrainzId != null && other.musicBrainzId != null ->
+ musicBrainzId == other.musicBrainzId
+ musicBrainzId == null && other.musicBrainzId == null ->
+ name.equals(other.name, true) && rawArtists == other.rawArtists
+ else -> false
+ }
}
}
@@ -1108,21 +916,19 @@ class Artist constructor(private val raw: Raw, songAlbums: List) : MusicP
override fun hashCode() = hashCode
- override fun equals(other: Any?): Boolean {
- if (other !is Raw) return false
-
- if (musicBrainzId != null &&
- other.musicBrainzId != null &&
- musicBrainzId == other.musicBrainzId) {
- return true
- }
-
- return when {
- name != null && other.name != null -> name.equals(other.name, true)
- name == null && other.name == null -> true
- else -> false
- }
- }
+ override fun equals(other: Any?) =
+ other is Raw &&
+ when {
+ musicBrainzId != null && other.musicBrainzId != null ->
+ musicBrainzId == other.musicBrainzId
+ musicBrainzId == null && other.musicBrainzId == null ->
+ when {
+ name != null && other.name != null -> name.equals(other.name, true)
+ name == null && other.name == null -> true
+ else -> false
+ }
+ else -> false
+ }
}
}
@@ -1217,7 +1023,7 @@ class Genre constructor(private val raw: Raw, override val songs: List) :
* @return A [UUID] converted from the [String] value, or null if the value was not valid.
* @see UUID.fromString
*/
-fun String.toUuidOrNull(): UUID? =
+private fun String.toUuidOrNull(): UUID? =
try {
UUID.fromString(this)
} catch (e: IllegalArgumentException) {
@@ -1228,7 +1034,8 @@ fun String.toUuidOrNull(): UUID? =
* Update a [MessageDigest] with a lowercase [String].
* @param string The [String] to hash. If null, it will not be hashed.
*/
-private fun MessageDigest.update(string: String?) {
+@VisibleForTesting
+fun MessageDigest.update(string: String?) {
if (string != null) {
update(string.lowercase().toByteArray())
} else {
@@ -1240,7 +1047,8 @@ private fun MessageDigest.update(string: String?) {
* Update a [MessageDigest] with the string representation of a [Date].
* @param date The [Date] to hash. If null, nothing will be done.
*/
-private fun MessageDigest.update(date: Date?) {
+@VisibleForTesting
+fun MessageDigest.update(date: Date?) {
if (date != null) {
update(date.toString().toByteArray())
} else {
@@ -1252,7 +1060,8 @@ private fun MessageDigest.update(date: Date?) {
* Update a [MessageDigest] with the lowercase versions of all of the input [String]s.
* @param strings The [String]s to hash. If a [String] is null, it will not be hashed.
*/
-private fun MessageDigest.update(strings: List) {
+@VisibleForTesting
+fun MessageDigest.update(strings: List) {
strings.forEach(::update)
}
@@ -1260,7 +1069,8 @@ private fun MessageDigest.update(strings: List) {
* Update a [MessageDigest] with the little-endian bytes of a [Int].
* @param n The [Int] to write. If null, nothing will be done.
*/
-private fun MessageDigest.update(n: Int?) {
+@VisibleForTesting
+fun MessageDigest.update(n: Int?) {
if (n != null) {
update(byteArrayOf(n.toByte(), n.shr(8).toByte(), n.shr(16).toByte(), n.shr(24).toByte()))
} else {
diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt
new file mode 100644
index 000000000..b96a97fbd
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt
@@ -0,0 +1,224 @@
+/*
+ * 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