extends CoordinatorLayo
gestureInsetBottom = mandatoryGestureInsets.bottom;
}
- // Don't update the peek height to be above the navigation bar or gestures if these
- // flags are off. It means the client is already handling it.
- if (paddingBottomSystemWindowInsets || shouldHandleGestureInsets) {
- updatePeekHeight(/* animate= */ false);
- }
+ // MODIFICATION: Fix awful assumption that clients handling edge-to-edge by themselves
+ // don't need peek height adjustments (Despite the fact that they still likely padding
+ // the view, just without clipping anything)
+ updatePeekHeight(/* animate= */ false);
return insets;
}
});
diff --git a/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetDialog.java b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetDialog.java
new file mode 100644
index 000000000..060fe04d2
--- /dev/null
+++ b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetDialog.java
@@ -0,0 +1,549 @@
+/*
+ * Copyright (C) 2015 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.bottomsheet;
+
+import com.google.android.material.R;
+
+import static com.google.android.material.color.MaterialColors.isColorLight;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Build;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import androidx.appcompat.app.AppCompatDialog;
+import android.util.TypedValue;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager.LayoutParams;
+import android.widget.FrameLayout;
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleRes;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import androidx.core.view.AccessibilityDelegateCompat;
+import androidx.core.view.OnApplyWindowInsetsListener;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowCompat;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.core.view.WindowInsetsControllerCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import com.google.android.material.internal.EdgeToEdgeUtils;
+import com.google.android.material.motion.MaterialBackOrchestrator;
+import com.google.android.material.shape.MaterialShapeDrawable;
+
+/**
+ * Base class for {@link android.app.Dialog}s styled as a bottom sheet.
+ *
+ * Edge to edge window flags are automatically applied if the {@link
+ * android.R.attr#navigationBarColor} is transparent or translucent and {@code enableEdgeToEdge} is
+ * true. These can be set in the theme that is passed to the constructor, or will be taken from the
+ * theme of the context (ie. your application or activity theme).
+ *
+ *
In edge to edge mode, padding will be added automatically to the top when sliding under the
+ * status bar. Padding can be applied automatically to the left, right, or bottom if any of
+ * `paddingBottomSystemWindowInsets`, `paddingLeftSystemWindowInsets`, or
+ * `paddingRightSystemWindowInsets` are set to true in the style.
+ *
+ * MODIFICATION: Replace all usages of BottomSheetBehavior with BackportBottomSheetBehavior
+ */
+public class BackportBottomSheetDialog extends AppCompatDialog {
+
+ private BackportBottomSheetBehavior behavior;
+
+ private FrameLayout container;
+ private CoordinatorLayout coordinator;
+ private FrameLayout bottomSheet;
+
+ boolean dismissWithAnimation;
+
+ boolean cancelable = true;
+ private boolean canceledOnTouchOutside = true;
+ private boolean canceledOnTouchOutsideSet;
+ private EdgeToEdgeCallback edgeToEdgeCallback;
+ private boolean edgeToEdgeEnabled;
+ @Nullable private MaterialBackOrchestrator backOrchestrator;
+
+ public BackportBottomSheetDialog(@NonNull Context context) {
+ this(context, 0);
+
+ edgeToEdgeEnabled =
+ getContext()
+ .getTheme()
+ .obtainStyledAttributes(new int[] {R.attr.enableEdgeToEdge})
+ .getBoolean(0, false);
+ }
+
+ public BackportBottomSheetDialog(@NonNull Context context, @StyleRes int theme) {
+ super(context, getThemeResId(context, theme));
+ // We hide the title bar for any style configuration. Otherwise, there will be a gap
+ // above the bottom sheet when it is expanded.
+ supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
+
+ edgeToEdgeEnabled =
+ getContext()
+ .getTheme()
+ .obtainStyledAttributes(new int[] {R.attr.enableEdgeToEdge})
+ .getBoolean(0, false);
+ }
+
+ protected BackportBottomSheetDialog(
+ @NonNull Context context, boolean cancelable, OnCancelListener cancelListener) {
+ super(context, cancelable, cancelListener);
+ supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
+ this.cancelable = cancelable;
+
+ edgeToEdgeEnabled =
+ getContext()
+ .getTheme()
+ .obtainStyledAttributes(new int[] {R.attr.enableEdgeToEdge})
+ .getBoolean(0, false);
+ }
+
+ @Override
+ public void setContentView(@LayoutRes int layoutResId) {
+ super.setContentView(wrapInBottomSheet(layoutResId, null, null));
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Window window = getWindow();
+ if (window != null) {
+ if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+ // The status bar should always be transparent because of the window animation.
+ window.setStatusBarColor(0);
+
+ window.addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+ if (VERSION.SDK_INT < VERSION_CODES.M) {
+ // It can be transparent for API 23 and above because we will handle switching the status
+ // bar icons to light or dark as appropriate. For API 21 and API 22 we just set the
+ // translucent status bar.
+ window.addFlags(LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ }
+ }
+ window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+ }
+ }
+
+ @Override
+ public void setContentView(View view) {
+ super.setContentView(wrapInBottomSheet(0, view, null));
+ }
+
+ @Override
+ public void setContentView(View view, ViewGroup.LayoutParams params) {
+ super.setContentView(wrapInBottomSheet(0, view, params));
+ }
+
+ @Override
+ public void setCancelable(boolean cancelable) {
+ super.setCancelable(cancelable);
+ if (this.cancelable != cancelable) {
+ this.cancelable = cancelable;
+ if (behavior != null) {
+ behavior.setHideable(cancelable);
+ }
+ if (getWindow() != null) {
+ updateListeningForBackCallbacks();
+ }
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (behavior != null && behavior.getState() == BackportBottomSheetBehavior.STATE_HIDDEN) {
+ behavior.setState(BackportBottomSheetBehavior.STATE_COLLAPSED);
+ }
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ Window window = getWindow();
+ if (window != null) {
+ if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+ // If the navigation bar is transparent at all the BottomSheet should be edge to edge.
+ boolean drawEdgeToEdge =
+ edgeToEdgeEnabled && Color.alpha(window.getNavigationBarColor()) < 255;
+ if (container != null) {
+ container.setFitsSystemWindows(!drawEdgeToEdge);
+ }
+ if (coordinator != null) {
+ coordinator.setFitsSystemWindows(!drawEdgeToEdge);
+ }
+ WindowCompat.setDecorFitsSystemWindows(window, !drawEdgeToEdge);
+ }
+ if (edgeToEdgeCallback != null) {
+ edgeToEdgeCallback.setWindow(window);
+ }
+ }
+
+ updateListeningForBackCallbacks();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ if (edgeToEdgeCallback != null) {
+ edgeToEdgeCallback.setWindow(null);
+ }
+
+ if (backOrchestrator != null) {
+ backOrchestrator.stopListeningForBackCallbacks();
+ }
+ }
+
+ /**
+ * This function can be called from a few different use cases, including Swiping the dialog down
+ * or calling `dismiss()` from a `BackportBottomSheetDialogFragment`, tapping outside a dialog, etc...
+ *
+ * The default animation to dismiss this dialog is a fade-out transition through a
+ * windowAnimation. Call {@link #setDismissWithAnimation(true)} if you want to utilize the
+ * BottomSheet animation instead.
+ *
+ *
If this function is called from a swipe down interaction, or dismissWithAnimation is false,
+ * then keep the default behavior.
+ *
+ *
Else, since this is a terminal event which will finish this dialog, we override the attached
+ * {@link BackportBottomSheetBehavior.BottomSheetCallback} to call this function, after {@link
+ * BackportBottomSheetBehavior#STATE_HIDDEN} is set. This will enforce the swipe down animation before
+ * canceling this dialog.
+ */
+ @Override
+ public void cancel() {
+ BackportBottomSheetBehavior behavior = getBehavior();
+
+ if (!dismissWithAnimation || behavior.getState() == BackportBottomSheetBehavior.STATE_HIDDEN) {
+ super.cancel();
+ } else {
+ behavior.setState(BackportBottomSheetBehavior.STATE_HIDDEN);
+ }
+ }
+
+ @Override
+ public void setCanceledOnTouchOutside(boolean cancel) {
+ super.setCanceledOnTouchOutside(cancel);
+ if (cancel && !cancelable) {
+ cancelable = true;
+ }
+ canceledOnTouchOutside = cancel;
+ canceledOnTouchOutsideSet = true;
+ }
+
+ @NonNull
+ public BackportBottomSheetBehavior getBehavior() {
+ if (behavior == null) {
+ // The content hasn't been set, so the behavior doesn't exist yet. Let's create it.
+ ensureContainerAndBehavior();
+ }
+ return behavior;
+ }
+
+ /**
+ * Set to perform the swipe down animation when dismissing instead of the window animation for the
+ * dialog.
+ *
+ * @param dismissWithAnimation True if swipe down animation should be used when dismissing.
+ */
+ public void setDismissWithAnimation(boolean dismissWithAnimation) {
+ this.dismissWithAnimation = dismissWithAnimation;
+ }
+
+ /**
+ * Returns if dismissing will perform the swipe down animation on the bottom sheet, rather than
+ * the window animation for the dialog.
+ */
+ public boolean getDismissWithAnimation() {
+ return dismissWithAnimation;
+ }
+
+ /** Returns if edge to edge behavior is enabled for this dialog. */
+ public boolean getEdgeToEdgeEnabled() {
+ return edgeToEdgeEnabled;
+ }
+
+ /** Creates the container layout which must exist to find the behavior */
+ private FrameLayout ensureContainerAndBehavior() {
+ if (container == null) {
+ container =
+ (FrameLayout) View.inflate(getContext(), R.layout.design_bottom_sheet_dialog, null);
+
+ coordinator = (CoordinatorLayout) container.findViewById(R.id.coordinator);
+ bottomSheet = (FrameLayout) container.findViewById(R.id.design_bottom_sheet);
+
+ // MODIFICATION: Override layout-specified BottomSheetBehavior w/BackportBottomSheetBehavior
+ behavior = BackportBottomSheetBehavior.from(bottomSheet);
+ behavior.addBottomSheetCallback(bottomSheetCallback);
+ behavior.setHideable(cancelable);
+
+ backOrchestrator = new MaterialBackOrchestrator(behavior, bottomSheet);
+ }
+ return container;
+ }
+
+ private View wrapInBottomSheet(
+ int layoutResId, @Nullable View view, @Nullable ViewGroup.LayoutParams params) {
+ ensureContainerAndBehavior();
+ CoordinatorLayout coordinator = (CoordinatorLayout) container.findViewById(R.id.coordinator);
+ if (layoutResId != 0 && view == null) {
+ view = getLayoutInflater().inflate(layoutResId, coordinator, false);
+ }
+
+ if (edgeToEdgeEnabled) {
+ ViewCompat.setOnApplyWindowInsetsListener(
+ bottomSheet,
+ new OnApplyWindowInsetsListener() {
+ @Override
+ public WindowInsetsCompat onApplyWindowInsets(View view, WindowInsetsCompat insets) {
+ if (edgeToEdgeCallback != null) {
+ behavior.removeBottomSheetCallback(edgeToEdgeCallback);
+ }
+
+ if (insets != null) {
+ edgeToEdgeCallback = new EdgeToEdgeCallback(bottomSheet, insets);
+ edgeToEdgeCallback.setWindow(getWindow());
+ behavior.addBottomSheetCallback(edgeToEdgeCallback);
+ }
+
+ return insets;
+ }
+ });
+ }
+
+ bottomSheet.removeAllViews();
+ if (params == null) {
+ bottomSheet.addView(view);
+ } else {
+ bottomSheet.addView(view, params);
+ }
+ // We treat the CoordinatorLayout as outside the dialog though it is technically inside
+ coordinator
+ .findViewById(R.id.touch_outside)
+ .setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (cancelable && isShowing() && shouldWindowCloseOnTouchOutside()) {
+ cancel();
+ }
+ }
+ });
+ // Handle accessibility events
+ ViewCompat.setAccessibilityDelegate(
+ bottomSheet,
+ new AccessibilityDelegateCompat() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(
+ View host, @NonNull AccessibilityNodeInfoCompat info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ if (cancelable) {
+ info.addAction(AccessibilityNodeInfoCompat.ACTION_DISMISS);
+ info.setDismissable(true);
+ } else {
+ info.setDismissable(false);
+ }
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ if (action == AccessibilityNodeInfoCompat.ACTION_DISMISS && cancelable) {
+ cancel();
+ return true;
+ }
+ return super.performAccessibilityAction(host, action, args);
+ }
+ });
+ bottomSheet.setOnTouchListener(
+ new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ // Consume the event and prevent it from falling through
+ return true;
+ }
+ });
+ return container;
+ }
+
+ private void updateListeningForBackCallbacks() {
+ if (backOrchestrator == null) {
+ return;
+ }
+ if (cancelable) {
+ backOrchestrator.startListeningForBackCallbacks();
+ } else {
+ backOrchestrator.stopListeningForBackCallbacks();
+ }
+ }
+
+ boolean shouldWindowCloseOnTouchOutside() {
+ if (!canceledOnTouchOutsideSet) {
+ TypedArray a =
+ getContext().obtainStyledAttributes(new int[] {android.R.attr.windowCloseOnTouchOutside});
+ canceledOnTouchOutside = a.getBoolean(0, true);
+ a.recycle();
+ canceledOnTouchOutsideSet = true;
+ }
+ return canceledOnTouchOutside;
+ }
+
+ private static int getThemeResId(@NonNull Context context, int themeId) {
+ if (themeId == 0) {
+ // If the provided theme is 0, then retrieve the dialogTheme from our theme
+ TypedValue outValue = new TypedValue();
+ if (context.getTheme().resolveAttribute(R.attr.bottomSheetDialogTheme, outValue, true)) {
+ themeId = outValue.resourceId;
+ } else {
+ // bottomSheetDialogTheme is not provided; we default to our light theme
+ themeId = R.style.Theme_Design_Light_BottomSheetDialog;
+ }
+ }
+ return themeId;
+ }
+
+ void removeDefaultCallback() {
+ behavior.removeBottomSheetCallback(bottomSheetCallback);
+ }
+
+ @NonNull
+ private BackportBottomSheetBehavior.BottomSheetCallback bottomSheetCallback =
+ new BackportBottomSheetBehavior.BottomSheetCallback() {
+ @Override
+ public void onStateChanged(
+ @NonNull View bottomSheet, @BackportBottomSheetBehavior.State int newState) {
+ if (newState == BackportBottomSheetBehavior.STATE_HIDDEN) {
+ cancel();
+ }
+ }
+
+ @Override
+ public void onSlide(@NonNull View bottomSheet, float slideOffset) {}
+ };
+
+ private static class EdgeToEdgeCallback extends BackportBottomSheetBehavior.BottomSheetCallback {
+
+ @Nullable private final Boolean lightBottomSheet;
+ @NonNull private final WindowInsetsCompat insetsCompat;
+
+ @Nullable private Window window;
+ private boolean lightStatusBar;
+
+ private EdgeToEdgeCallback(
+ @NonNull final View bottomSheet, @NonNull WindowInsetsCompat insetsCompat) {
+ this.insetsCompat = insetsCompat;
+
+ // Try to find the background color to automatically change the status bar icons so they will
+ // still be visible when the bottomsheet slides underneath the status bar.
+ ColorStateList backgroundTint;
+ MaterialShapeDrawable msd = BackportBottomSheetBehavior.from(bottomSheet).getMaterialShapeDrawable();
+ if (msd != null) {
+ backgroundTint = msd.getFillColor();
+ } else {
+ backgroundTint = ViewCompat.getBackgroundTintList(bottomSheet);
+ }
+
+ if (backgroundTint != null) {
+ // First check for a tint
+ lightBottomSheet = isColorLight(backgroundTint.getDefaultColor());
+ } else if (bottomSheet.getBackground() instanceof ColorDrawable) {
+ // Then check for the background color
+ lightBottomSheet = isColorLight(((ColorDrawable) bottomSheet.getBackground()).getColor());
+ } else {
+ // Otherwise don't change the status bar color
+ lightBottomSheet = null;
+ }
+ }
+
+ @Override
+ public void onStateChanged(@NonNull View bottomSheet, int newState) {
+ setPaddingForPosition(bottomSheet);
+ }
+
+ @Override
+ public void onSlide(@NonNull View bottomSheet, float slideOffset) {
+ setPaddingForPosition(bottomSheet);
+ }
+
+ @Override
+ void onLayout(@NonNull View bottomSheet) {
+ setPaddingForPosition(bottomSheet);
+ }
+
+ void setWindow(@Nullable Window window) {
+ if (this.window == window) {
+ return;
+ }
+ this.window = window;
+ if (window != null) {
+ WindowInsetsControllerCompat insetsController =
+ WindowCompat.getInsetsController(window, window.getDecorView());
+ lightStatusBar = insetsController.isAppearanceLightStatusBars();
+ }
+ }
+
+ private void setPaddingForPosition(View bottomSheet) {
+ if (bottomSheet.getTop() < insetsCompat.getSystemWindowInsetTop()) {
+ // If the bottomsheet is light, we should set light status bar so the icons are visible
+ // since the bottomsheet is now under the status bar.
+ if (window != null) {
+ EdgeToEdgeUtils.setLightStatusBar(
+ window, lightBottomSheet == null ? lightStatusBar : lightBottomSheet);
+ }
+ // Smooth transition into status bar when drawing edge to edge.
+ bottomSheet.setPadding(
+ bottomSheet.getPaddingLeft(),
+ (insetsCompat.getSystemWindowInsetTop() - bottomSheet.getTop()),
+ bottomSheet.getPaddingRight(),
+ bottomSheet.getPaddingBottom());
+ } else if (bottomSheet.getTop() != 0) {
+ // Reset the status bar icons to the original color because the bottomsheet is not under the
+ // status bar.
+ if (window != null) {
+ EdgeToEdgeUtils.setLightStatusBar(window, lightStatusBar);
+ }
+ bottomSheet.setPadding(
+ bottomSheet.getPaddingLeft(),
+ 0,
+ bottomSheet.getPaddingRight(),
+ bottomSheet.getPaddingBottom());
+ }
+ }
+ }
+
+ /**
+ * @deprecated use {@link EdgeToEdgeUtils#setLightStatusBar(Window, boolean)} instead
+ */
+ @Deprecated
+ public static void setLightStatusBar(@NonNull View view, boolean isLight) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ int flags = view.getSystemUiVisibility();
+ if (isLight) {
+ flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+ } else {
+ flags &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+ }
+ view.setSystemUiVisibility(flags);
+ }
+ }
+}
diff --git a/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetDialogFragment.java b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetDialogFragment.java
new file mode 100644
index 000000000..eead66daa
--- /dev/null
+++ b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetDialogFragment.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2015 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.bottomsheet;
+
+import android.annotation.SuppressLint;
+import android.app.Dialog;
+import android.os.Bundle;
+import androidx.appcompat.app.AppCompatDialogFragment;
+import android.view.View;
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Modal bottom sheet. This is a version of {@link androidx.fragment.app.DialogFragment} that shows
+ * a bottom sheet using {@link BackportBottomSheetDialog} instead of a floating dialog.
+ */
+public class BackportBottomSheetDialogFragment extends AppCompatDialogFragment {
+
+ /**
+ * Tracks if we are waiting for a dismissAllowingStateLoss or a regular dismiss once the
+ * BottomSheet is hidden and onStateChanged() is called.
+ */
+ private boolean waitingForDismissAllowingStateLoss;
+
+ public BackportBottomSheetDialogFragment() {}
+
+ @SuppressLint("ValidFragment")
+ public BackportBottomSheetDialogFragment(@LayoutRes int contentLayoutId) {
+ super(contentLayoutId);
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ return new BackportBottomSheetDialog(getContext(), getTheme());
+ }
+
+ @Override
+ public void dismiss() {
+ if (!tryDismissWithAnimation(false)) {
+ super.dismiss();
+ }
+ }
+
+ @Override
+ public void dismissAllowingStateLoss() {
+ if (!tryDismissWithAnimation(true)) {
+ super.dismissAllowingStateLoss();
+ }
+ }
+
+ /**
+ * Tries to dismiss the dialog fragment with the bottom sheet animation. Returns true if possible,
+ * false otherwise.
+ */
+ private boolean tryDismissWithAnimation(boolean allowingStateLoss) {
+ Dialog baseDialog = getDialog();
+ if (baseDialog instanceof BackportBottomSheetDialog) {
+ BackportBottomSheetDialog dialog = (BackportBottomSheetDialog) baseDialog;
+ BackportBottomSheetBehavior> behavior = dialog.getBehavior();
+ if (behavior.isHideable() && dialog.getDismissWithAnimation()) {
+ dismissWithAnimation(behavior, allowingStateLoss);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void dismissWithAnimation(
+ @NonNull BackportBottomSheetBehavior> behavior, boolean allowingStateLoss) {
+ waitingForDismissAllowingStateLoss = allowingStateLoss;
+
+ if (behavior.getState() == BackportBottomSheetBehavior.STATE_HIDDEN) {
+ dismissAfterAnimation();
+ } else {
+ if (getDialog() instanceof BackportBottomSheetDialog) {
+ ((BackportBottomSheetDialog) getDialog()).removeDefaultCallback();
+ }
+ behavior.addBottomSheetCallback(new BottomSheetDismissCallback());
+ behavior.setState(BackportBottomSheetBehavior.STATE_HIDDEN);
+ }
+ }
+
+ private void dismissAfterAnimation() {
+ if (waitingForDismissAllowingStateLoss) {
+ super.dismissAllowingStateLoss();
+ } else {
+ super.dismiss();
+ }
+ }
+
+ private class BottomSheetDismissCallback extends BackportBottomSheetBehavior.BottomSheetCallback {
+
+ @Override
+ public void onStateChanged(@NonNull View bottomSheet, int newState) {
+ if (newState == BackportBottomSheetBehavior.STATE_HIDDEN) {
+ dismissAfterAnimation();
+ }
+ }
+
+ @Override
+ public void onSlide(@NonNull View bottomSheet, float slideOffset) {}
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/Auxio.kt b/app/src/main/java/org/oxycblt/auxio/Auxio.kt
index df737e4c2..ebcffb5e2 100644
--- a/app/src/main/java/org/oxycblt/auxio/Auxio.kt
+++ b/app/src/main/java/org/oxycblt/auxio/Auxio.kt
@@ -29,6 +29,7 @@ import org.oxycblt.auxio.home.HomeSettings
import org.oxycblt.auxio.image.ImageSettings
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.ui.UISettings
+import timber.log.Timber
/**
* A simple, rational music player for android.
@@ -44,6 +45,10 @@ class Auxio : Application() {
override fun onCreate() {
super.onCreate()
+ if (BuildConfig.DEBUG) {
+ Timber.plant(Timber.DebugTree())
+ }
+
// Migrate any settings that may have changed in an app update.
imageSettings.migrate()
playbackSettings.migrate()
diff --git a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt
index d0bff5315..54d59eb50 100644
--- a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt
+++ b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt
@@ -65,14 +65,14 @@ object IntegerTable {
const val REPEAT_MODE_ALL = 0xA101
/** RepeatMode.TRACK */
const val REPEAT_MODE_TRACK = 0xA102
- /** PlaybackMode.IN_GENRE */
- const val PLAYBACK_MODE_IN_GENRE = 0xA103
- /** PlaybackMode.IN_ARTIST */
- const val PLAYBACK_MODE_IN_ARTIST = 0xA104
- /** PlaybackMode.IN_ALBUM */
- const val PLAYBACK_MODE_IN_ALBUM = 0xA105
- /** PlaybackMode.ALL_SONGS */
- const val PLAYBACK_MODE_ALL_SONGS = 0xA106
+ // /** PlaybackMode.IN_GENRE (No longer used but still reserved) */
+ // const val PLAYBACK_MODE_IN_GENRE = 0xA103
+ // /** PlaybackMode.IN_ARTIST (No longer used but still reserved) */
+ // const val PLAYBACK_MODE_IN_ARTIST = 0xA104
+ // /** PlaybackMode.IN_ALBUM (No longer used but still reserved) */
+ // const val PLAYBACK_MODE_IN_ALBUM = 0xA105
+ // /** PlaybackMode.ALL_SONGS (No longer used but still reserved) */
+ // const val PLAYBACK_MODE_ALL_SONGS = 0xA106
/** MusicMode.SONGS */
const val MUSIC_MODE_SONGS = 0xA10B
/** MusicMode.ALBUMS */
@@ -101,8 +101,6 @@ object IntegerTable {
const val SORT_BY_TRACK = 0xA117
/** Sort.Mode.ByDateAdded */
const val SORT_BY_DATE_ADDED = 0xA118
- /** Sort.Mode.None */
- const val SORT_BY_NONE = 0xA11F
/** ReplayGainMode.Off (No longer used but still reserved) */
// const val REPLAY_GAIN_MODE_OFF = 0xA110
/** ReplayGainMode.Track */
@@ -123,4 +121,16 @@ object IntegerTable {
const val COVER_MODE_MEDIA_STORE = 0xA11D
/** CoverMode.Quality */
const val COVER_MODE_QUALITY = 0xA11E
+ /** PlaySong.FromAll */
+ const val PLAY_SONG_FROM_ALL = 0xA11F
+ /** PlaySong.FromAlbum */
+ const val PLAY_SONG_FROM_ALBUM = 0xA120
+ /** PlaySong.FromArtist */
+ const val PLAY_SONG_FROM_ARTIST = 0xA121
+ /** PlaySong.FromGenre */
+ const val PLAY_SONG_FROM_GENRE = 0xA122
+ /** PlaySong.FromPlaylist */
+ const val PLAY_SONG_FROM_PLAYLIST = 0xA123
+ /** PlaySong.ByItself */
+ const val PLAY_SONG_BY_ITSELF = 0xA124
}
diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt
index 725f60444..c98d89cdd 100644
--- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt
+++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt
@@ -68,8 +68,8 @@ class MainActivity : AppCompatActivity() {
logD("Activity created")
}
- override fun onStart() {
- super.onStart()
+ override fun onResume() {
+ super.onResume()
startService(Intent(this, IndexerService::class.java))
startService(Intent(this, PlaybackService::class.java))
diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt
index 9b6b47a08..ed1b47c7a 100644
--- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt
@@ -26,11 +26,9 @@ import androidx.activity.OnBackPressedCallback
import androidx.core.view.ViewCompat
import androidx.core.view.isInvisible
import androidx.core.view.updatePadding
-import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.activityViewModels
-import androidx.navigation.NavController
-import androidx.navigation.NavDestination
import androidx.navigation.findNavController
+import androidx.navigation.fragment.findNavController
import com.google.android.material.R as MR
import com.google.android.material.bottomsheet.BackportBottomSheetBehavior
import com.google.android.material.shape.MaterialShapeDrawable
@@ -40,44 +38,47 @@ import kotlin.math.max
import kotlin.math.min
import org.oxycblt.auxio.databinding.FragmentMainBinding
import org.oxycblt.auxio.detail.DetailViewModel
-import org.oxycblt.auxio.list.selection.SelectionViewModel
+import org.oxycblt.auxio.detail.Show
+import org.oxycblt.auxio.home.HomeViewModel
+import org.oxycblt.auxio.home.Outer
+import org.oxycblt.auxio.list.ListViewModel
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song
-import org.oxycblt.auxio.playback.Panel
+import org.oxycblt.auxio.playback.OpenPanel
import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior
+import org.oxycblt.auxio.ui.DialogAwareNavigationListener
import org.oxycblt.auxio.ui.ViewBindingFragment
+import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.coordinatorLayoutBehavior
import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getDimen
import org.oxycblt.auxio.util.logD
+import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.systemBarInsetsCompat
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
- * A wrapper around the home fragment that shows the playback fragment and controls the more
- * high-level navigation features.
+ * A wrapper around the home fragment that shows the playback fragment and high-level navigation.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class MainFragment :
- ViewBindingFragment(),
- ViewTreeObserver.OnPreDrawListener,
- NavController.OnDestinationChangedListener {
- private val playbackModel: PlaybackViewModel by activityViewModels()
- private val selectionModel: SelectionViewModel by activityViewModels()
+ ViewBindingFragment(), ViewTreeObserver.OnPreDrawListener {
private val detailModel: DetailViewModel by activityViewModels()
+ private val homeModel: HomeViewModel by activityViewModels()
+ private val listModel: ListViewModel by activityViewModels()
+ private val playbackModel: PlaybackViewModel by activityViewModels()
private var sheetBackCallback: SheetBackPressedCallback? = null
private var detailBackCallback: DetailBackPressedCallback? = null
private var selectionBackCallback: SelectionBackPressedCallback? = null
- private var exploreBackCallback: ExploreBackPressedCallback? = null
+ private var selectionNavigationListener: DialogAwareNavigationListener? = null
private var lastInsets: WindowInsets? = null
private var elevationNormal = 0f
- private var initialNavDestinationChange = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -100,28 +101,19 @@ class MainFragment :
// Currently all back press callbacks are handled in MainFragment, as it's not guaranteed
// that instantiating these callbacks in their respective fragments would result in the
// correct order.
- val sheetBackCallback =
+ sheetBackCallback =
SheetBackPressedCallback(
- playbackSheetBehavior = playbackSheetBehavior,
- queueSheetBehavior = queueSheetBehavior)
- .also { sheetBackCallback = it }
+ playbackSheetBehavior = playbackSheetBehavior,
+ queueSheetBehavior = queueSheetBehavior)
val detailBackCallback =
DetailBackPressedCallback(detailModel).also { detailBackCallback = it }
val selectionBackCallback =
- SelectionBackPressedCallback(selectionModel).also { selectionBackCallback = it }
- val exploreBackCallback =
- ExploreBackPressedCallback(binding.exploreNavHost).also { exploreBackCallback = it }
+ SelectionBackPressedCallback(listModel).also { selectionBackCallback = it }
+
+ selectionNavigationListener = DialogAwareNavigationListener(listModel::dropSelection)
// --- UI SETUP ---
val context = requireActivity()
- // Override the back pressed listener so we can map back navigation to collapsing
- // navigation, navigation out of detail views, etc.
- context.onBackPressedDispatcher.apply {
- addCallback(viewLifecycleOwner, exploreBackCallback)
- addCallback(viewLifecycleOwner, selectionBackCallback)
- addCallback(viewLifecycleOwner, detailBackCallback)
- addCallback(viewLifecycleOwner, sheetBackCallback)
- }
binding.root.setOnApplyWindowInsetsListener { _, insets ->
lastInsets = insets
@@ -159,8 +151,14 @@ class MainFragment :
}
// --- VIEWMODEL SETUP ---
+ // This has to be done here instead of the playback panel to make sure that it's prioritized
+ // by StateFlow over any detail fragment.
+ // FIXME: This is a consequence of sharing events across several consumers. There has to be
+ // a better way of doing this.
+ collect(detailModel.toShow.flow, ::handleShow)
collectImmediately(detailModel.editedPlaylist, detailBackCallback::invalidateEnabled)
- collectImmediately(selectionModel.selected, selectionBackCallback::invalidateEnabled)
+ collectImmediately(homeModel.showOuter.flow, ::handleShowOuter)
+ collectImmediately(listModel.selected, selectionBackCallback::invalidateEnabled)
collectImmediately(playbackModel.song, ::updateSong)
collectImmediately(playbackModel.openPanel.flow, ::handlePanel)
}
@@ -170,17 +168,30 @@ class MainFragment :
val binding = requireBinding()
// Once we add the destination change callback, we will receive another initialization call,
// so handle that by resetting the flag.
- initialNavDestinationChange = false
- binding.exploreNavHost.findNavController().addOnDestinationChangedListener(this)
+ requireNotNull(selectionNavigationListener) { "NavigationListener was not available" }
+ .attach(binding.exploreNavHost.findNavController())
// Listener could still reasonably fire even if we clear the binding, attach/detach
// our pre-draw listener our listener in onStart/onStop respectively.
binding.playbackSheet.viewTreeObserver.addOnPreDrawListener(this@MainFragment)
}
+ override fun onResume() {
+ super.onResume()
+ // Override the back pressed listener so we can map back navigation to collapsing
+ // navigation, navigation out of detail views, etc. We have to do this here in
+ // onResume or otherwise the FragmentManager will have precedence.
+ requireActivity().onBackPressedDispatcher.apply {
+ addCallback(viewLifecycleOwner, requireNotNull(selectionBackCallback))
+ addCallback(viewLifecycleOwner, requireNotNull(detailBackCallback))
+ addCallback(viewLifecycleOwner, requireNotNull(sheetBackCallback))
+ }
+ }
+
override fun onStop() {
super.onStop()
val binding = requireBinding()
- binding.exploreNavHost.findNavController().removeOnDestinationChangedListener(this)
+ requireNotNull(selectionNavigationListener) { "NavigationListener was not available" }
+ .release(binding.exploreNavHost.findNavController())
binding.playbackSheet.viewTreeObserver.removeOnPreDrawListener(this)
}
@@ -189,10 +200,14 @@ class MainFragment :
sheetBackCallback = null
detailBackCallback = null
selectionBackCallback = null
- exploreBackCallback = null
+ selectionNavigationListener = null
}
override fun onPreDraw(): Boolean {
+ // TODO: Due to draw caching even *this* isn't effective enough to avoid the bottom
+ // sheets continually getting stuck. I need something with even more frequent updates,
+ // or otherwise bottom sheets get stuck.
+
// We overload CoordinatorLayout far too much to rely on any of it's typical
// listener functionality. Just update all transitions before every draw. Should
// probably be cheap enough.
@@ -283,21 +298,29 @@ class MainFragment :
return true
}
- override fun onDestinationChanged(
- controller: NavController,
- destination: NavDestination,
- arguments: Bundle?
- ) {
- // Drop the initial call by NavController that simply provides us with the current
- // destination. This would cause the selection state to be lost every time the device
- // rotates.
- requireNotNull(exploreBackCallback) { "ExploreBackPressedCallback was not available" }
- .invalidateEnabled()
- if (!initialNavDestinationChange) {
- initialNavDestinationChange = true
- return
+ private fun handleShow(show: Show?) {
+ when (show) {
+ is Show.SongAlbumDetails,
+ is Show.ArtistDetails,
+ is Show.AlbumDetails -> playbackModel.openMain()
+ is Show.SongDetails,
+ is Show.SongArtistDecision,
+ is Show.AlbumArtistDecision,
+ is Show.GenreDetails,
+ is Show.PlaylistDetails,
+ null -> {}
}
- selectionModel.drop()
+ }
+
+ private fun handleShowOuter(outer: Outer?) {
+ val directions =
+ when (outer) {
+ is Outer.Settings -> MainFragmentDirections.preferences()
+ is Outer.About -> MainFragmentDirections.about()
+ null -> return
+ }
+ findNavController().navigateSafe(directions)
+ homeModel.showOuter.consume()
}
private fun updateSong(song: Song?) {
@@ -308,13 +331,13 @@ class MainFragment :
}
}
- private fun handlePanel(panel: Panel?) {
+ private fun handlePanel(panel: OpenPanel?) {
if (panel == null) return
logD("Trying to update panel to $panel")
when (panel) {
- is Panel.Main -> tryClosePlaybackPanel()
- is Panel.Playback -> tryOpenPlaybackPanel()
- is Panel.Queue -> tryOpenQueuePanel()
+ OpenPanel.MAIN -> tryClosePlaybackPanel()
+ OpenPanel.PLAYBACK -> tryOpenPlaybackPanel()
+ OpenPanel.QUEUE -> tryOpenQueuePanel()
}
playbackModel.openPanel.consume()
}
@@ -458,11 +481,10 @@ class MainFragment :
}
}
- private inner class SelectionBackPressedCallback(
- private val selectionModel: SelectionViewModel
- ) : OnBackPressedCallback(false) {
+ private inner class SelectionBackPressedCallback(private val listModel: ListViewModel) :
+ OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
- if (selectionModel.drop()) {
+ if (listModel.dropSelection()) {
logD("Dropped selection")
}
}
@@ -471,23 +493,4 @@ class MainFragment :
isEnabled = selection.isNotEmpty()
}
}
-
- private inner class ExploreBackPressedCallback(
- private val exploreNavHost: FragmentContainerView
- ) : OnBackPressedCallback(false) {
- // Note: We cannot cache the NavController in a variable since it's current destination
- // value goes stale for some reason.
-
- override fun handleOnBackPressed() {
- exploreNavHost.findNavController().navigateUp()
- logD("Forwarded back navigation to explore nav host")
- }
-
- fun invalidateEnabled() {
- val exploreNavController = exploreNavHost.findNavController()
- isEnabled =
- exploreNavController.currentDestination?.id !=
- exploreNavController.graph.startDestinationId
- }
- }
}
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt
index 8bf635e5e..3fd4a6963 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt
@@ -20,8 +20,6 @@ package org.oxycblt.auxio.detail
import android.os.Bundle
import android.view.LayoutInflater
-import android.view.MenuItem
-import android.view.View
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@@ -39,26 +37,24 @@ import org.oxycblt.auxio.list.Divider
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment
-import org.oxycblt.auxio.list.Sort
-import org.oxycblt.auxio.list.selection.SelectionViewModel
+import org.oxycblt.auxio.list.ListViewModel
+import org.oxycblt.auxio.list.menu.Menu
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Music
-import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.PlaylistDecision
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.info.Disc
+import org.oxycblt.auxio.playback.PlaybackDecision
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.canScroll
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
-import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.navigateSafe
+import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
import org.oxycblt.auxio.util.setFullWidthLookup
-import org.oxycblt.auxio.util.share
-import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@@ -71,10 +67,11 @@ class AlbumDetailFragment :
ListFragment(),
AlbumDetailHeaderAdapter.Listener,
DetailListAdapter.Listener {
- override val detailModel: DetailViewModel by activityViewModels()
- override val selectionModel: SelectionViewModel by activityViewModels()
+ private val detailModel: DetailViewModel by activityViewModels()
+ override val listModel: ListViewModel by activityViewModels()
override val musicModel: MusicViewModel by activityViewModels()
override val playbackModel: PlaybackViewModel by activityViewModels()
+
// Information about what album to display is initially within the navigation arguments
// as a UID, as that is the only safe way to parcel an album.
private val args: AlbumDetailFragmentArgs by navArgs()
@@ -101,16 +98,18 @@ class AlbumDetailFragment :
// --- UI SETUP --
binding.detailNormalToolbar.apply {
- inflateMenu(R.menu.menu_album_detail)
setNavigationOnClickListener { findNavController().navigateUp() }
- setOnMenuItemClickListener(this@AlbumDetailFragment)
+ overrideOnOverflowMenuClick {
+ listModel.openMenu(
+ R.menu.detail_album, unlikelyToBeNull(detailModel.currentAlbum.value))
+ }
}
binding.detailRecycler.apply {
adapter = ConcatAdapter(albumHeaderAdapter, albumListAdapter)
(layoutManager as GridLayoutManager).setFullWidthLookup {
if (it != 0) {
- val item = detailModel.albumList.value[it - 1]
+ val item = detailModel.albumSongList.value[it - 1]
item is Divider || item is Header || item is Disc
} else {
true
@@ -122,14 +121,14 @@ class AlbumDetailFragment :
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setAlbum(args.albumUid)
collectImmediately(detailModel.currentAlbum, ::updateAlbum)
- collectImmediately(detailModel.albumList, ::updateList)
+ collectImmediately(detailModel.albumSongList, ::updateList)
collect(detailModel.toShow.flow, ::handleShow)
- collectImmediately(selectionModel.selected, ::updateSelection)
- collect(musicModel.playlistDecision.flow, ::handleDecision)
+ collect(listModel.menu.flow, ::handleMenu)
+ collectImmediately(listModel.selected, ::updateSelection)
+ collect(musicModel.playlistDecision.flow, ::handlePlaylistDecision)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
- collect(playbackModel.artistPickerSong.flow, ::handlePlayFromArtist)
- collect(playbackModel.genrePickerSong.flow, ::handlePlayFromGenre)
+ collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision)
}
override fun onDestroyBinding(binding: FragmentDetailBinding) {
@@ -138,52 +137,15 @@ class AlbumDetailFragment :
binding.detailRecycler.adapter = null
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
// during list initialization and crash the app. Could happen if the user is fast enough.
- detailModel.albumInstructions.consume()
- }
-
- override fun onMenuItemClick(item: MenuItem): Boolean {
- if (super.onMenuItemClick(item)) {
- return true
- }
-
- val currentAlbum = unlikelyToBeNull(detailModel.currentAlbum.value)
- return when (item.itemId) {
- R.id.action_play_next -> {
- playbackModel.playNext(currentAlbum)
- requireContext().showToast(R.string.lng_queue_added)
- true
- }
- R.id.action_queue_add -> {
- playbackModel.addToQueue(currentAlbum)
- requireContext().showToast(R.string.lng_queue_added)
- true
- }
- R.id.action_go_artist -> {
- onNavigateToParentArtist()
- true
- }
- R.id.action_playlist_add -> {
- musicModel.addToPlaylist(currentAlbum)
- true
- }
- R.id.action_share -> {
- requireContext().share(currentAlbum)
- true
- }
- else -> {
- logW("Unexpected menu item selected")
- false
- }
- }
+ detailModel.albumSongInstructions.consume()
}
override fun onRealClick(item: Song) {
- // There can only be one album, so a null mode and an ALBUMS mode will function the same.
- playbackModel.playFrom(item, detailModel.playbackMode ?: MusicMode.ALBUMS)
+ playbackModel.play(item, detailModel.playInAlbumWith)
}
- override fun onOpenMenu(item: Song, anchor: View) {
- openMusicMenu(anchor, R.menu.menu_album_song_actions, item)
+ override fun onOpenMenu(item: Song) {
+ listModel.openMenu(R.menu.album_song, item, detailModel.playInAlbumWith)
}
override fun onPlay() {
@@ -194,31 +156,8 @@ class AlbumDetailFragment :
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentAlbum.value))
}
- override fun onOpenSortMenu(anchor: View) {
- openMenu(anchor, R.menu.menu_album_sort) {
- // Select the corresponding sort mode option
- val sort = detailModel.albumSongSort
- unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
- // Select the corresponding sort direction option
- val directionItemId =
- when (sort.direction) {
- Sort.Direction.ASCENDING -> R.id.option_sort_asc
- Sort.Direction.DESCENDING -> R.id.option_sort_dec
- }
- unlikelyToBeNull(menu.findItem(directionItemId)).isChecked = true
- setOnMenuItemClickListener { item ->
- item.isChecked = !item.isChecked
- detailModel.albumSongSort =
- when (item.itemId) {
- // Sort direction options
- R.id.option_sort_asc -> sort.withDirection(Sort.Direction.ASCENDING)
- R.id.option_sort_dec -> sort.withDirection(Sort.Direction.DESCENDING)
- // Any other option is a sort mode
- else -> sort.withMode(unlikelyToBeNull(Sort.Mode.fromItemId(item.itemId)))
- }
- true
- }
- }
+ override fun onOpenSortMenu() {
+ findNavController().navigateSafe(AlbumDetailFragmentDirections.sort())
}
override fun onNavigateToParentArtist() {
@@ -236,7 +175,7 @@ class AlbumDetailFragment :
}
private fun updateList(list: List- ) {
- albumListAdapter.update(list, detailModel.albumInstructions.consume())
+ albumListAdapter.update(list, detailModel.albumSongInstructions.consume())
}
private fun handleShow(show: Show?) {
@@ -275,22 +214,20 @@ class AlbumDetailFragment :
.navigateSafe(AlbumDetailFragmentDirections.showAlbum(show.album.uid))
}
}
-
- // Always launch a new ArtistDetailFragment.
is Show.ArtistDetails -> {
logD("Navigating to ${show.artist}")
findNavController()
.navigateSafe(AlbumDetailFragmentDirections.showArtist(show.artist.uid))
}
- is Show.SongArtistDetails -> {
+ is Show.SongArtistDecision -> {
logD("Navigating to artist choices for ${show.song}")
findNavController()
- .navigateSafe(AlbumDetailFragmentDirections.showArtist(show.song.uid))
+ .navigateSafe(AlbumDetailFragmentDirections.showArtistChoices(show.song.uid))
}
- is Show.AlbumArtistDetails -> {
+ is Show.AlbumArtistDecision -> {
logD("Navigating to artist choices for ${show.album}")
findNavController()
- .navigateSafe(AlbumDetailFragmentDirections.showArtist(show.album.uid))
+ .navigateSafe(AlbumDetailFragmentDirections.showArtistChoices(show.album.uid))
}
is Show.GenreDetails,
is Show.PlaylistDetails -> {
@@ -300,6 +237,20 @@ class AlbumDetailFragment :
}
}
+ private fun handleMenu(menu: Menu?) {
+ if (menu == null) return
+ val directions =
+ when (menu) {
+ is Menu.ForSong -> AlbumDetailFragmentDirections.openSongMenu(menu.parcel)
+ is Menu.ForAlbum -> AlbumDetailFragmentDirections.openAlbumMenu(menu.parcel)
+ is Menu.ForSelection -> AlbumDetailFragmentDirections.openSelectionMenu(menu.parcel)
+ is Menu.ForArtist,
+ is Menu.ForGenre,
+ is Menu.ForPlaylist -> error("Unexpected menu $menu")
+ }
+ findNavController().navigateSafe(directions)
+ }
+
private fun updateSelection(selected: List) {
albumListAdapter.setSelected(selected.toSet())
@@ -312,21 +263,20 @@ class AlbumDetailFragment :
}
}
- private fun handleDecision(decision: PlaylistDecision?) {
- when (decision) {
- is PlaylistDecision.Add -> {
- logD("Adding ${decision.songs.size} songs to a playlist")
- findNavController()
- .navigateSafe(
- AlbumDetailFragmentDirections.addToPlaylist(
- decision.songs.map { it.uid }.toTypedArray()))
- musicModel.playlistDecision.consume()
+ private fun handlePlaylistDecision(decision: PlaylistDecision?) {
+ if (decision == null) return
+ val directions =
+ when (decision) {
+ is PlaylistDecision.Add -> {
+ logD("Adding ${decision.songs.size} songs to a playlist")
+ AlbumDetailFragmentDirections.addToPlaylist(
+ decision.songs.map { it.uid }.toTypedArray())
+ }
+ is PlaylistDecision.New,
+ is PlaylistDecision.Rename,
+ is PlaylistDecision.Delete -> error("Unexpected playlist decision $decision")
}
- is PlaylistDecision.New,
- is PlaylistDecision.Rename,
- is PlaylistDecision.Delete -> error("Unexpected decision $decision")
- null -> {}
- }
+ findNavController().navigateSafe(directions)
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
@@ -334,21 +284,25 @@ class AlbumDetailFragment :
song.takeIf { parent == detailModel.currentAlbum.value }, isPlaying)
}
- private fun handlePlayFromArtist(song: Song?) {
- if (song == null) return
- logD("Launching play from artist dialog for $song")
- findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromArtist(song.uid))
- }
-
- private fun handlePlayFromGenre(song: Song?) {
- if (song == null) return
- logD("Launching play from genre dialog for $song")
- findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromGenre(song.uid))
+ private fun handlePlaybackDecision(decision: PlaybackDecision?) {
+ if (decision == null) return
+ val directions =
+ when (decision) {
+ is PlaybackDecision.PlayFromArtist -> {
+ logD("Launching play from artist dialog for $decision")
+ AlbumDetailFragmentDirections.playFromArtist(decision.song.uid)
+ }
+ is PlaybackDecision.PlayFromGenre -> {
+ logD("Launching play from artist dialog for $decision")
+ AlbumDetailFragmentDirections.playFromGenre(decision.song.uid)
+ }
+ }
+ findNavController().navigateSafe(directions)
}
private fun scrollToAlbumSong(song: Song) {
// Calculate where the item for the currently played song is
- val pos = detailModel.albumList.value.indexOf(song)
+ val pos = detailModel.albumSongList.value.indexOf(song)
if (pos != -1) {
// Only scroll if the song is within this album.
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt
index 86208424b..b0bc09386 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt
@@ -20,8 +20,6 @@ package org.oxycblt.auxio.detail
import android.os.Bundle
import android.view.LayoutInflater
-import android.view.MenuItem
-import android.view.View
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@@ -39,8 +37,8 @@ import org.oxycblt.auxio.list.Divider
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment
-import org.oxycblt.auxio.list.Sort
-import org.oxycblt.auxio.list.selection.SelectionViewModel
+import org.oxycblt.auxio.list.ListViewModel
+import org.oxycblt.auxio.list.menu.Menu
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
@@ -48,15 +46,14 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.PlaylistDecision
import org.oxycblt.auxio.music.Song
+import org.oxycblt.auxio.playback.PlaybackDecision
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
-import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.navigateSafe
+import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
import org.oxycblt.auxio.util.setFullWidthLookup
-import org.oxycblt.auxio.util.share
-import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@@ -69,8 +66,8 @@ class ArtistDetailFragment :
ListFragment(),
DetailHeaderAdapter.Listener,
DetailListAdapter.Listener {
- override val detailModel: DetailViewModel by activityViewModels()
- override val selectionModel: SelectionViewModel by activityViewModels()
+ private val detailModel: DetailViewModel by activityViewModels()
+ override val listModel: ListViewModel by activityViewModels()
override val musicModel: MusicViewModel by activityViewModels()
override val playbackModel: PlaybackViewModel by activityViewModels()
// Information about what artist to display is initially within the navigation arguments
@@ -99,9 +96,12 @@ class ArtistDetailFragment :
// --- UI SETUP ---
binding.detailNormalToolbar.apply {
- inflateMenu(R.menu.menu_parent_detail)
setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener(this@ArtistDetailFragment)
+ overrideOnOverflowMenuClick {
+ listModel.openMenu(
+ R.menu.detail_parent, unlikelyToBeNull(detailModel.currentArtist.value))
+ }
}
binding.detailRecycler.apply {
@@ -109,7 +109,7 @@ class ArtistDetailFragment :
(layoutManager as GridLayoutManager).setFullWidthLookup {
if (it != 0) {
val item =
- detailModel.artistList.value.getOrElse(it - 1) {
+ detailModel.artistSongList.value.getOrElse(it - 1) {
return@setFullWidthLookup false
}
item is Divider || item is Header
@@ -123,14 +123,14 @@ class ArtistDetailFragment :
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setArtist(args.artistUid)
collectImmediately(detailModel.currentArtist, ::updateArtist)
- collectImmediately(detailModel.artistList, ::updateList)
+ collectImmediately(detailModel.artistSongList, ::updateList)
collect(detailModel.toShow.flow, ::handleShow)
- collectImmediately(selectionModel.selected, ::updateSelection)
- collect(musicModel.playlistDecision.flow, ::handleDecision)
+ collect(listModel.menu.flow, ::handleMenu)
+ collectImmediately(listModel.selected, ::updateSelection)
+ collect(musicModel.playlistDecision.flow, ::handlePlaylistDecision)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
- collect(playbackModel.artistPickerSong.flow, ::handlePlayFromArtist)
- collect(playbackModel.genrePickerSong.flow, ::handlePlayFromGenre)
+ collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision)
}
override fun onDestroyBinding(binding: FragmentDetailBinding) {
@@ -139,63 +139,21 @@ class ArtistDetailFragment :
binding.detailRecycler.adapter = null
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
// during list initialization and crash the app. Could happen if the user is fast enough.
- detailModel.artistInstructions.consume()
- }
-
- override fun onMenuItemClick(item: MenuItem): Boolean {
- if (super.onMenuItemClick(item)) {
- return true
- }
-
- val currentArtist = unlikelyToBeNull(detailModel.currentArtist.value)
- return when (item.itemId) {
- R.id.action_play_next -> {
- playbackModel.playNext(currentArtist)
- requireContext().showToast(R.string.lng_queue_added)
- true
- }
- R.id.action_queue_add -> {
- playbackModel.addToQueue(currentArtist)
- requireContext().showToast(R.string.lng_queue_added)
- true
- }
- R.id.action_playlist_add -> {
- musicModel.addToPlaylist(currentArtist)
- true
- }
- R.id.action_share -> {
- requireContext().share(currentArtist)
- true
- }
- else -> {
- logW("Unexpected menu item selected")
- false
- }
- }
+ detailModel.artistSongInstructions.consume()
}
override fun onRealClick(item: Music) {
when (item) {
is Album -> detailModel.showAlbum(item)
- is Song -> {
- val playbackMode = detailModel.playbackMode
- if (playbackMode != null) {
- playbackModel.playFrom(item, playbackMode)
- } else {
- // When configured to play from the selected item, we already have an Artist
- // to play from.
- playbackModel.playFromArtist(
- item, unlikelyToBeNull(detailModel.currentArtist.value))
- }
- }
+ is Song -> playbackModel.play(item, detailModel.playInArtistWith)
else -> error("Unexpected datatype: ${item::class.simpleName}")
}
}
- override fun onOpenMenu(item: Music, anchor: View) {
+ override fun onOpenMenu(item: Music) {
when (item) {
- is Song -> openMusicMenu(anchor, R.menu.menu_artist_song_actions, item)
- is Album -> openMusicMenu(anchor, R.menu.menu_artist_album_actions, item)
+ is Song -> listModel.openMenu(R.menu.artist_song, item, detailModel.playInArtistWith)
+ is Album -> listModel.openMenu(R.menu.artist_album, item)
else -> error("Unexpected datatype: ${item::class.simpleName}")
}
}
@@ -208,33 +166,8 @@ class ArtistDetailFragment :
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentArtist.value))
}
- override fun onOpenSortMenu(anchor: View) {
- openMenu(anchor, R.menu.menu_artist_sort) {
- // Select the corresponding sort mode option
- val sort = detailModel.artistSongSort
- unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
- // Select the corresponding sort direction option
- val directionItemId =
- when (sort.direction) {
- Sort.Direction.ASCENDING -> R.id.option_sort_asc
- Sort.Direction.DESCENDING -> R.id.option_sort_dec
- }
- unlikelyToBeNull(menu.findItem(directionItemId)).isChecked = true
- setOnMenuItemClickListener { item ->
- item.isChecked = !item.isChecked
-
- detailModel.artistSongSort =
- when (item.itemId) {
- // Sort direction options
- R.id.option_sort_asc -> sort.withDirection(Sort.Direction.ASCENDING)
- R.id.option_sort_dec -> sort.withDirection(Sort.Direction.DESCENDING)
- // Any other option is a sort mode
- else -> sort.withMode(unlikelyToBeNull(Sort.Mode.fromItemId(item.itemId)))
- }
-
- true
- }
- }
+ override fun onOpenSortMenu() {
+ findNavController().navigateSafe(ArtistDetailFragmentDirections.sort())
}
private fun updateArtist(artist: Artist?) {
@@ -243,24 +176,12 @@ class ArtistDetailFragment :
findNavController().navigateUp()
return
}
- requireBinding().detailNormalToolbar.apply {
- title = artist.name.resolve(requireContext())
-
- // Disable options that make no sense with an empty artist
- val playable = artist.songs.isNotEmpty()
- if (!playable) {
- logD("Artist is empty, disabling playback/playlist/share options")
- }
- menu.findItem(R.id.action_play_next).isEnabled = playable
- menu.findItem(R.id.action_queue_add).isEnabled = playable
- menu.findItem(R.id.action_playlist_add).isEnabled = playable
- menu.findItem(R.id.action_share).isEnabled = playable
- }
+ requireBinding().detailNormalToolbar.title = artist.name.resolve(requireContext())
artistHeaderAdapter.setParent(artist)
}
private fun updateList(list: List
- ) {
- artistListAdapter.update(list, detailModel.artistInstructions.consume())
+ artistListAdapter.update(list, detailModel.artistSongInstructions.consume())
}
private fun handleShow(show: Show?) {
@@ -300,8 +221,16 @@ class ArtistDetailFragment :
.navigateSafe(ArtistDetailFragmentDirections.showArtist(show.artist.uid))
}
}
- is Show.SongArtistDetails,
- is Show.AlbumArtistDetails,
+ is Show.SongArtistDecision -> {
+ logD("Navigating to artist choices for ${show.song}")
+ findNavController()
+ .navigateSafe(ArtistDetailFragmentDirections.showArtistChoices(show.song.uid))
+ }
+ is Show.AlbumArtistDecision -> {
+ logD("Navigating to artist choices for ${show.album}")
+ findNavController()
+ .navigateSafe(ArtistDetailFragmentDirections.showArtistChoices(show.album.uid))
+ }
is Show.GenreDetails,
is Show.PlaylistDetails -> {
error("Unexpected show command $show")
@@ -310,6 +239,21 @@ class ArtistDetailFragment :
}
}
+ private fun handleMenu(menu: Menu?) {
+ if (menu == null) return
+ val directions =
+ when (menu) {
+ is Menu.ForSong -> ArtistDetailFragmentDirections.openSongMenu(menu.parcel)
+ is Menu.ForAlbum -> ArtistDetailFragmentDirections.openAlbumMenu(menu.parcel)
+ is Menu.ForArtist -> ArtistDetailFragmentDirections.openArtistMenu(menu.parcel)
+ is Menu.ForSelection ->
+ ArtistDetailFragmentDirections.openSelectionMenu(menu.parcel)
+ is Menu.ForGenre,
+ is Menu.ForPlaylist -> error("Unexpected menu $menu")
+ }
+ findNavController().navigateSafe(directions)
+ }
+
private fun updateSelection(selected: List) {
artistListAdapter.setSelected(selected.toSet())
@@ -322,21 +266,20 @@ class ArtistDetailFragment :
}
}
- private fun handleDecision(decision: PlaylistDecision?) {
- when (decision) {
- is PlaylistDecision.Add -> {
- logD("Adding ${decision.songs.size} songs to a playlist")
- findNavController()
- .navigateSafe(
- ArtistDetailFragmentDirections.addToPlaylist(
- decision.songs.map { it.uid }.toTypedArray()))
- musicModel.playlistDecision.consume()
+ private fun handlePlaylistDecision(decision: PlaylistDecision?) {
+ if (decision == null) return
+ val directions =
+ when (decision) {
+ is PlaylistDecision.Add -> {
+ logD("Adding ${decision.songs.size} songs to a playlist")
+ ArtistDetailFragmentDirections.addToPlaylist(
+ decision.songs.map { it.uid }.toTypedArray())
+ }
+ is PlaylistDecision.New,
+ is PlaylistDecision.Rename,
+ is PlaylistDecision.Delete -> error("Unexpected playlist decision $decision")
}
- is PlaylistDecision.New,
- is PlaylistDecision.Rename,
- is PlaylistDecision.Delete -> error("Unexpected decision $decision")
- null -> {}
- }
+ findNavController().navigateSafe(directions)
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
@@ -354,15 +297,17 @@ class ArtistDetailFragment :
artistListAdapter.setPlaying(playingItem, isPlaying)
}
- private fun handlePlayFromArtist(song: Song?) {
- if (song == null) return
- logD("Launching play from artist dialog for $song")
- findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromArtist(song.uid))
- }
-
- private fun handlePlayFromGenre(song: Song?) {
- if (song == null) return
- logD("Launching play from genre dialog for $song")
- findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromGenre(song.uid))
+ private fun handlePlaybackDecision(decision: PlaybackDecision?) {
+ if (decision == null) return
+ val directions =
+ when (decision) {
+ is PlaybackDecision.PlayFromArtist ->
+ error("Unexpected playback decision $decision")
+ is PlaybackDecision.PlayFromGenre -> {
+ logD("Launching play from artist dialog for $decision")
+ ArtistDetailFragmentDirections.playFromGenre(decision.song.uid)
+ }
+ }
+ findNavController().navigateSafe(directions)
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt
index 28c1f65f7..3c494cd96 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt
@@ -59,7 +59,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
override fun onAttachedToWindow() {
super.onAttachedToWindow()
- (layoutParams as CoordinatorLayout.LayoutParams).behavior = Behavior(context)
+ if (!isInEditMode) {
+ (layoutParams as CoordinatorLayout.LayoutParams).behavior = Behavior(context)
+ }
}
private fun findTitleView(): TextView {
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt
index de285ffa9..3613d96c6 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt
@@ -36,24 +36,25 @@ import org.oxycblt.auxio.detail.list.SortHeader
import org.oxycblt.auxio.list.BasicHeader
import org.oxycblt.auxio.list.Divider
import org.oxycblt.auxio.list.Item
-import org.oxycblt.auxio.list.Sort
+import org.oxycblt.auxio.list.ListSettings
import org.oxycblt.auxio.list.adapter.UpdateInstructions
+import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
-import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicRepository
-import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.info.ReleaseType
import org.oxycblt.auxio.music.metadata.AudioProperties
+import org.oxycblt.auxio.playback.PlaySong
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.util.Event
import org.oxycblt.auxio.util.MutableEvent
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
+import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* [ViewModel] that manages the Song, Album, Artist, and Genre detail views. Keeps track of the
@@ -65,12 +66,15 @@ import org.oxycblt.auxio.util.logW
class DetailViewModel
@Inject
constructor(
+ private val listSettings: ListSettings,
private val musicRepository: MusicRepository,
private val audioPropertiesFactory: AudioProperties.Factory,
- private val musicSettings: MusicSettings,
private val playbackSettings: PlaybackSettings
) : ViewModel(), MusicRepository.UpdateListener {
private val _toShow = MutableEvent()
+ /**
+ * A [Show] command that is awaiting a view capable of responding to it. Null if none currently.
+ */
val toShow: Event
get() = _toShow
@@ -94,23 +98,23 @@ constructor(
val currentAlbum: StateFlow
get() = _currentAlbum
- private val _albumList = MutableStateFlow(listOf
- ())
+ private val _albumSongList = MutableStateFlow(listOf
- ())
/** The current list data derived from [currentAlbum]. */
- val albumList: StateFlow
>
- get() = _albumList
- private val _albumInstructions = MutableEvent()
- /** Instructions for updating [albumList] in the UI. */
- val albumInstructions: Event
- get() = _albumInstructions
+ val albumSongList: StateFlow>
+ get() = _albumSongList
- /** The current [Sort] used for [Song]s in [albumList]. */
- var albumSongSort: Sort
- get() = musicSettings.albumSongSort
- set(value) {
- musicSettings.albumSongSort = value
- // Refresh the album list to reflect the new sort.
- currentAlbum.value?.let { refreshAlbumList(it, true) }
- }
+ private val _albumSongInstructions = MutableEvent()
+ /** Instructions for updating [albumSongList] in the UI. */
+ val albumSongInstructions: Event
+ get() = _albumSongInstructions
+
+ /** The current [Sort] used for [Song]s in [albumSongList]. */
+ val albumSongSort: Sort
+ get() = listSettings.albumSongSort
+
+ /** The [PlaySong] instructions to use when playing a [Song] from [Album] details. */
+ val playInAlbumWith
+ get() = playbackSettings.inParentPlaybackMode ?: PlaySong.FromAlbum
// --- ARTIST ---
@@ -119,23 +123,28 @@ constructor(
val currentArtist: StateFlow
get() = _currentArtist
- private val _artistList = MutableStateFlow(listOf- ())
+ private val _artistSongList = MutableStateFlow(listOf
- ())
/** The current list derived from [currentArtist]. */
- val artistList: StateFlow
> = _artistList
- private val _artistInstructions = MutableEvent()
- /** Instructions for updating [artistList] in the UI. */
- val artistInstructions: Event
- get() = _artistInstructions
+ val artistSongList: StateFlow> = _artistSongList
- /** The current [Sort] used for [Song]s in [artistList]. */
+ private val _artistSongInstructions = MutableEvent()
+ /** Instructions for updating [artistSongList] in the UI. */
+ val artistSongInstructions: Event
+ get() = _artistSongInstructions
+
+ /** The current [Sort] used for [Song]s in [artistSongList]. */
var artistSongSort: Sort
- get() = musicSettings.artistSongSort
+ get() = listSettings.artistSongSort
set(value) {
- musicSettings.artistSongSort = value
+ listSettings.artistSongSort = value
// Refresh the artist list to reflect the new sort.
currentArtist.value?.let { refreshArtistList(it, true) }
}
+ /** The [PlaySong] instructions to use when playing a [Song] from [Artist] details. */
+ val playInArtistWith
+ get() = playbackSettings.inParentPlaybackMode ?: PlaySong.FromArtist(currentArtist.value)
+
// --- GENRE ---
private val _currentGenre = MutableStateFlow(null)
@@ -143,23 +152,28 @@ constructor(
val currentGenre: StateFlow
get() = _currentGenre
- private val _genreList = MutableStateFlow(listOf- ())
+ private val _genreSongList = MutableStateFlow(listOf
- ())
/** The current list data derived from [currentGenre]. */
- val genreList: StateFlow
> = _genreList
- private val _genreInstructions = MutableEvent()
- /** Instructions for updating [artistList] in the UI. */
- val genreInstructions: Event
- get() = _genreInstructions
+ val genreSongList: StateFlow> = _genreSongList
- /** The current [Sort] used for [Song]s in [genreList]. */
+ private val _genreSongInstructions = MutableEvent()
+ /** Instructions for updating [artistSongList] in the UI. */
+ val genreSongInstructions: Event
+ get() = _genreSongInstructions
+
+ /** The current [Sort] used for [Song]s in [genreSongList]. */
var genreSongSort: Sort
- get() = musicSettings.genreSongSort
+ get() = listSettings.genreSongSort
set(value) {
- musicSettings.genreSongSort = value
+ listSettings.genreSongSort = value
// Refresh the genre list to reflect the new sort.
currentGenre.value?.let { refreshGenreList(it, true) }
}
+ /** The [PlaySong] instructions to use when playing a [Song] from [Genre] details. */
+ val playInGenreWith
+ get() = playbackSettings.inParentPlaybackMode ?: PlaySong.FromGenre(currentGenre.value)
+
// --- PLAYLIST ---
private val _currentPlaylist = MutableStateFlow(null)
@@ -167,13 +181,14 @@ constructor(
val currentPlaylist: StateFlow
get() = _currentPlaylist
- private val _playlistList = MutableStateFlow(listOf- ())
+ private val _playlistSongList = MutableStateFlow(listOf
- ())
/** The current list data derived from [currentPlaylist] */
- val playlistList: StateFlow
> = _playlistList
- private val _playlistInstructions = MutableEvent()
- /** Instructions for updating [playlistList] in the UI. */
- val playlistInstructions: Event
- get() = _playlistInstructions
+ val playlistSongList: StateFlow> = _playlistSongList
+
+ private val _playlistSongInstructions = MutableEvent()
+ /** Instructions for updating [playlistSongList] in the UI. */
+ val playlistSongInstructions: Event
+ get() = _playlistSongInstructions
private val _editedPlaylist = MutableStateFlow?>(null)
/**
@@ -183,12 +198,11 @@ constructor(
val editedPlaylist: StateFlow?>
get() = _editedPlaylist
- /**
- * The [MusicMode] to use when playing a [Song] from the UI, or null to play from the currently
- * shown item.
- */
- val playbackMode: MusicMode?
- get() = playbackSettings.inParentPlaybackMode
+ /** The [PlaySong] instructions to use when playing a [Song] from [Genre] details. */
+ val playInPlaylistWith
+ get() =
+ playbackSettings.inParentPlaybackMode
+ ?: PlaySong.FromPlaylist(unlikelyToBeNull(currentPlaylist.value))
init {
musicRepository.addUpdateListener(this)
@@ -241,32 +255,74 @@ constructor(
}
}
+ /**
+ * Navigate to the details (properties) of a [Song].
+ *
+ * @param song The [Song] to navigate with.
+ */
fun showSong(song: Song) = showImpl(Show.SongDetails(song))
+ /**
+ * Navigate to the [Album] details of the given [Song], scrolling to the given [Song] as well.
+ *
+ * @param song The [Song] to navigate with.
+ */
fun showAlbum(song: Song) = showImpl(Show.SongAlbumDetails(song))
+ /**
+ * Navigate to the details of an [Album].
+ *
+ * @param album The [Album] to navigate with.
+ */
fun showAlbum(album: Album) = showImpl(Show.AlbumDetails(album))
+ /**
+ * Navigate to the details of one of the [Artist]s of a [Song] using the corresponding choice
+ * dialog. If there is only one artist, this call is identical to [showArtist].
+ *
+ * @param song The [Song] to navigate with.
+ */
fun showArtist(song: Song) =
showImpl(
if (song.artists.size > 1) {
- Show.SongArtistDetails(song)
+ Show.SongArtistDecision(song)
} else {
Show.ArtistDetails(song.artists.first())
})
+ /**
+ * Navigate to the details of one of the [Artist]s of an [Album] using the corresponding choice
+ * dialog. If there is only one artist, this call is identical to [showArtist].
+ *
+ * @param album The [Album] to navigate with.
+ */
fun showArtist(album: Album) =
showImpl(
if (album.artists.size > 1) {
- Show.AlbumArtistDetails(album)
+ Show.AlbumArtistDecision(album)
} else {
Show.ArtistDetails(album.artists.first())
})
+ /**
+ * Navigate to the details of an [Artist].
+ *
+ * @param artist The [Artist] to navigate with.
+ */
fun showArtist(artist: Artist) = showImpl(Show.ArtistDetails(artist))
+ /**
+ * Navigate to the details of a [Genre].
+ *
+ * @param genre The [Genre] to navigate with.
+ */
fun showGenre(genre: Genre) = showImpl(Show.GenreDetails(genre))
+ /**
+ * Navigate to the details of a [Playlist].
+ *
+ * @param playlist The [Playlist] to navigate with.
+ */
fun showPlaylist(playlist: Playlist) = showImpl(Show.PlaylistDetails(playlist))
private fun showImpl(show: Show) {
@@ -293,7 +349,7 @@ constructor(
}
/**
- * Set a new [currentAlbum] from it's [Music.UID]. [currentAlbum] and [albumList] will be
+ * Set a new [currentAlbum] from it's [Music.UID]. [currentAlbum] and [albumSongList] will be
* updated to align with the new [Album].
*
* @param uid The [Music.UID] of the [Album] to update [currentAlbum] to. Must be valid.
@@ -308,7 +364,17 @@ constructor(
}
/**
- * Set a new [currentArtist] from it's [Music.UID]. [currentArtist] and [artistList] will be
+ * Apply a new [Sort] to [albumSongList].
+ *
+ * @param sort The [Sort] to apply.
+ */
+ fun applyAlbumSongSort(sort: Sort) {
+ listSettings.albumSongSort = sort
+ _currentAlbum.value?.let { refreshAlbumList(it, true) }
+ }
+
+ /**
+ * Set a new [currentArtist] from it's [Music.UID]. [currentArtist] and [artistSongList] will be
* updated to align with the new [Artist].
*
* @param uid The [Music.UID] of the [Artist] to update [currentArtist] to. Must be valid.
@@ -323,7 +389,17 @@ constructor(
}
/**
- * Set a new [currentGenre] from it's [Music.UID]. [currentGenre] and [genreList] will be
+ * Apply a new [Sort] to [artistSongList].
+ *
+ * @param sort The [Sort] to apply.
+ */
+ fun applyArtistSongSort(sort: Sort) {
+ listSettings.artistSongSort = sort
+ _currentArtist.value?.let { refreshArtistList(it, true) }
+ }
+
+ /**
+ * Set a new [currentGenre] from it's [Music.UID]. [currentGenre] and [genreSongList] will be
* updated to align with the new album.
*
* @param uid The [Music.UID] of the [Genre] to update [currentGenre] to. Must be valid.
@@ -337,6 +413,16 @@ constructor(
}
}
+ /**
+ * Apply a new [Sort] to [genreSongList].
+ *
+ * @param sort The [Sort] to apply.
+ */
+ fun applyGenreSongSort(sort: Sort) {
+ listSettings.genreSongSort = sort
+ _currentGenre.value?.let { refreshGenreList(it, true) }
+ }
+
/**
* Set a new [currentPlaylist] from it's [Music.UID]. If the [Music.UID] differs,
* [currentPlaylist] and [currentPlaylist] will be updated to align with the new album.
@@ -394,6 +480,17 @@ constructor(
return true
}
+ /**
+ * Apply a [Sort] to the edited playlist. Does nothing if not in an editing session.
+ *
+ * @param sort The [Sort] to apply.
+ */
+ fun applyPlaylistSongSort(sort: Sort) {
+ val playlist = _currentPlaylist.value ?: return
+ _editedPlaylist.value = sort.songs(_editedPlaylist.value ?: return)
+ refreshPlaylistList(playlist, UpdateInstructions.Replace(2))
+ }
+
/**
* (Visually) move a song in the current playlist. Does nothing if not in an editing session.
*
@@ -402,7 +499,6 @@ constructor(
* @return true if the song was moved, false otherwise.
*/
fun movePlaylistSongs(from: Int, to: Int): Boolean {
- // TODO: Song re-sorting
val playlist = _currentPlaylist.value ?: return false
val editedPlaylist = (_editedPlaylist.value ?: return false).toMutableList()
val realFrom = from - 2
@@ -486,8 +582,8 @@ constructor(
}
logD("Update album list to ${list.size} items with $instructions")
- _albumInstructions.put(instructions)
- _albumList.value = list
+ _albumSongInstructions.put(instructions)
+ _albumSongList.value = list
}
private fun refreshArtistList(artist: Artist, replace: Boolean = false) {
@@ -511,6 +607,7 @@ constructor(
is ReleaseType.Soundtrack -> AlbumGrouping.SOUNDTRACKS
is ReleaseType.Mix -> AlbumGrouping.DJMIXES
is ReleaseType.Mixtape -> AlbumGrouping.MIXTAPES
+ is ReleaseType.Demo -> AlbumGrouping.DEMOS
}
}
}
@@ -549,8 +646,8 @@ constructor(
}
logD("Updating artist list to ${list.size} items with $instructions")
- _artistInstructions.put(instructions)
- _artistList.value = list.toList()
+ _artistSongInstructions.put(instructions)
+ _artistSongList.value = list.toList()
}
private fun refreshGenreList(genre: Genre, replace: Boolean = false) {
@@ -575,8 +672,8 @@ constructor(
list.addAll(genreSongSort.songs(genre.songs))
logD("Updating genre list to ${list.size} items with $instructions")
- _genreInstructions.put(instructions)
- _genreList.value = list
+ _genreSongInstructions.put(instructions)
+ _genreSongList.value = list
}
private fun refreshPlaylistList(
@@ -595,8 +692,8 @@ constructor(
}
logD("Updating playlist list to ${list.size} items with $instructions")
- _playlistInstructions.put(instructions)
- _playlistList.value = list
+ _playlistSongInstructions.put(instructions)
+ _playlistSongList.value = list
}
/**
@@ -613,6 +710,7 @@ constructor(
SOUNDTRACKS(R.string.lbl_soundtracks),
DJMIXES(R.string.lbl_mixes),
MIXTAPES(R.string.lbl_mixtapes),
+ DEMOS(R.string.lbl_demos),
APPEARANCES(R.string.lbl_appears_on),
LIVE(R.string.lbl_live_group),
REMIXES(R.string.lbl_remix_group),
@@ -624,13 +722,68 @@ constructor(
}
}
+/**
+ * A command for navigation to detail views. These can be handled partially if a certain command
+ * cannot occur in a specific view.
+ *
+ * @author Alexander Capehart (OxygenCobalt)
+ */
sealed interface Show {
+ /**
+ * Navigate to the details (properties) of a [Song].
+ *
+ * @param song The [Song] to navigate with.
+ */
data class SongDetails(val song: Song) : Show
+
+ /**
+ * Navigate to the details of an [Album].
+ *
+ * @param album The [Album] to navigate with.
+ */
data class AlbumDetails(val album: Album) : Show
+
+ /**
+ * Navigate to the [Album] details of the given [Song], scrolling to the given [Song] as well.
+ *
+ * @param song The [Song] to navigate with.
+ */
data class SongAlbumDetails(val song: Song) : Show
+
+ /**
+ * Navigate to the details of an [Artist].
+ *
+ * @param artist The [Artist] to navigate with.
+ */
data class ArtistDetails(val artist: Artist) : Show
- data class SongArtistDetails(val song: Song) : Show
- data class AlbumArtistDetails(val album: Album) : Show
+
+ /**
+ * Navigate to the details of one of the [Artist]s of a [Song] using the corresponding choice
+ * dialog.
+ *
+ * @param song The [Song] to navigate with.
+ */
+ data class SongArtistDecision(val song: Song) : Show
+
+ /**
+ * Navigate to the details of one of the [Artist]s of an [Album] using the corresponding
+ * decision dialog.
+ *
+ * @param album The [Album] to navigate with.
+ */
+ data class AlbumArtistDecision(val album: Album) : Show
+
+ /**
+ * Navigate to the details of a [Genre].
+ *
+ * @param genre The [Genre] to navigate with.
+ */
data class GenreDetails(val genre: Genre) : Show
+
+ /**
+ * Navigate to the details of a [Playlist].
+ *
+ * @param playlist The [Playlist] to navigate with.
+ */
data class PlaylistDetails(val playlist: Playlist) : Show
}
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 a2d2e2cd9..522ebbfa6 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt
@@ -20,8 +20,6 @@ package org.oxycblt.auxio.detail
import android.os.Bundle
import android.view.LayoutInflater
-import android.view.MenuItem
-import android.view.View
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@@ -39,8 +37,8 @@ import org.oxycblt.auxio.list.Divider
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment
-import org.oxycblt.auxio.list.Sort
-import org.oxycblt.auxio.list.selection.SelectionViewModel
+import org.oxycblt.auxio.list.ListViewModel
+import org.oxycblt.auxio.list.menu.Menu
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
@@ -48,15 +46,14 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.PlaylistDecision
import org.oxycblt.auxio.music.Song
+import org.oxycblt.auxio.playback.PlaybackDecision
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
-import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.navigateSafe
+import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
import org.oxycblt.auxio.util.setFullWidthLookup
-import org.oxycblt.auxio.util.share
-import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@@ -69,8 +66,8 @@ class GenreDetailFragment :
ListFragment(),
DetailHeaderAdapter.Listener,
DetailListAdapter.Listener {
- override val detailModel: DetailViewModel by activityViewModels()
- override val selectionModel: SelectionViewModel by activityViewModels()
+ private val detailModel: DetailViewModel by activityViewModels()
+ override val listModel: ListViewModel by activityViewModels()
override val musicModel: MusicViewModel by activityViewModels()
override val playbackModel: PlaybackViewModel by activityViewModels()
// Information about what genre to display is initially within the navigation arguments
@@ -97,9 +94,12 @@ class GenreDetailFragment :
// --- UI SETUP ---
binding.detailNormalToolbar.apply {
- inflateMenu(R.menu.menu_parent_detail)
setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener(this@GenreDetailFragment)
+ overrideOnOverflowMenuClick {
+ listModel.openMenu(
+ R.menu.detail_parent, unlikelyToBeNull(detailModel.currentGenre.value))
+ }
}
binding.detailRecycler.apply {
@@ -107,7 +107,7 @@ class GenreDetailFragment :
(layoutManager as GridLayoutManager).setFullWidthLookup {
if (it != 0) {
val item =
- detailModel.genreList.value.getOrElse(it - 1) {
+ detailModel.genreSongList.value.getOrElse(it - 1) {
return@setFullWidthLookup false
}
item is Divider || item is Header
@@ -121,14 +121,14 @@ class GenreDetailFragment :
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setGenre(args.genreUid)
collectImmediately(detailModel.currentGenre, ::updatePlaylist)
- collectImmediately(detailModel.genreList, ::updateList)
+ collectImmediately(detailModel.genreSongList, ::updateList)
collect(detailModel.toShow.flow, ::handleShow)
- collectImmediately(selectionModel.selected, ::updateSelection)
+ collect(listModel.menu.flow, ::handleMenu)
+ collectImmediately(listModel.selected, ::updateSelection)
collect(musicModel.playlistDecision.flow, ::handleDecision)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
- collect(playbackModel.artistPickerSong.flow, ::handlePlayFromArtist)
- collect(playbackModel.genrePickerSong.flow, ::handlePlayFromGenre)
+ collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision)
}
override fun onDestroyBinding(binding: FragmentDetailBinding) {
@@ -137,63 +137,21 @@ class GenreDetailFragment :
binding.detailRecycler.adapter = null
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
// during list initialization and crash the app. Could happen if the user is fast enough.
- detailModel.genreInstructions.consume()
- }
-
- override fun onMenuItemClick(item: MenuItem): Boolean {
- if (super.onMenuItemClick(item)) {
- return true
- }
-
- val currentGenre = unlikelyToBeNull(detailModel.currentGenre.value)
- return when (item.itemId) {
- R.id.action_play_next -> {
- playbackModel.playNext(currentGenre)
- requireContext().showToast(R.string.lng_queue_added)
- true
- }
- R.id.action_queue_add -> {
- playbackModel.addToQueue(currentGenre)
- requireContext().showToast(R.string.lng_queue_added)
- true
- }
- R.id.action_playlist_add -> {
- musicModel.addToPlaylist(currentGenre)
- true
- }
- R.id.action_share -> {
- requireContext().share(currentGenre)
- true
- }
- else -> {
- logW("Unexpected menu item selected")
- false
- }
- }
+ detailModel.genreSongInstructions.consume()
}
override fun onRealClick(item: Music) {
when (item) {
is Artist -> detailModel.showArtist(item)
- is Song -> {
- val playbackMode = detailModel.playbackMode
- if (playbackMode != null) {
- playbackModel.playFrom(item, playbackMode)
- } else {
- // When configured to play from the selected item, we already have an Genre
- // to play from.
- playbackModel.playFromGenre(
- item, unlikelyToBeNull(detailModel.currentGenre.value))
- }
- }
+ is Song -> playbackModel.play(item, detailModel.playInGenreWith)
else -> error("Unexpected datatype: ${item::class.simpleName}")
}
}
- override fun onOpenMenu(item: Music, anchor: View) {
+ override fun onOpenMenu(item: Music) {
when (item) {
- is Artist -> openMusicMenu(anchor, R.menu.menu_parent_actions, item)
- is Song -> openMusicMenu(anchor, R.menu.menu_song_actions, item)
+ is Artist -> listModel.openMenu(R.menu.parent, item)
+ is Song -> listModel.openMenu(R.menu.song, item, detailModel.playInGenreWith)
else -> error("Unexpected datatype: ${item::class.simpleName}")
}
}
@@ -206,31 +164,8 @@ class GenreDetailFragment :
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentGenre.value))
}
- override fun onOpenSortMenu(anchor: View) {
- openMenu(anchor, R.menu.menu_genre_sort) {
- // Select the corresponding sort mode option
- val sort = detailModel.genreSongSort
- unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
- // Select the corresponding sort direction option
- val directionItemId =
- when (sort.direction) {
- Sort.Direction.ASCENDING -> R.id.option_sort_asc
- Sort.Direction.DESCENDING -> R.id.option_sort_dec
- }
- unlikelyToBeNull(menu.findItem(directionItemId)).isChecked = true
- setOnMenuItemClickListener { item ->
- item.isChecked = !item.isChecked
- detailModel.genreSongSort =
- when (item.itemId) {
- // Sort direction options
- R.id.option_sort_asc -> sort.withDirection(Sort.Direction.ASCENDING)
- R.id.option_sort_dec -> sort.withDirection(Sort.Direction.DESCENDING)
- // Any other option is a sort mode
- else -> sort.withMode(unlikelyToBeNull(Sort.Mode.fromItemId(item.itemId)))
- }
- true
- }
- }
+ override fun onOpenSortMenu() {
+ findNavController().navigateSafe(GenreDetailFragmentDirections.sort())
}
private fun updatePlaylist(genre: Genre?) {
@@ -244,7 +179,7 @@ class GenreDetailFragment :
}
private fun updateList(list: List- ) {
- genreListAdapter.update(list, detailModel.genreInstructions.consume())
+ genreListAdapter.update(list, detailModel.genreSongInstructions.consume())
}
private fun handleShow(show: Show?) {
@@ -277,15 +212,15 @@ class GenreDetailFragment :
findNavController()
.navigateSafe(GenreDetailFragmentDirections.showArtist(show.artist.uid))
}
- is Show.SongArtistDetails -> {
+ is Show.SongArtistDecision -> {
logD("Navigating to artist choices for ${show.song}")
findNavController()
- .navigateSafe(GenreDetailFragmentDirections.showArtist(show.song.uid))
+ .navigateSafe(GenreDetailFragmentDirections.showArtistChoices(show.song.uid))
}
- is Show.AlbumArtistDetails -> {
+ is Show.AlbumArtistDecision -> {
logD("Navigating to artist choices for ${show.album}")
findNavController()
- .navigateSafe(GenreDetailFragmentDirections.showArtist(show.album.uid))
+ .navigateSafe(GenreDetailFragmentDirections.showArtistChoices(show.album.uid))
}
is Show.GenreDetails -> {
logD("Navigated to this genre")
@@ -298,6 +233,20 @@ class GenreDetailFragment :
}
}
+ private fun handleMenu(menu: Menu?) {
+ if (menu == null) return
+ val directions =
+ when (menu) {
+ is Menu.ForSong -> GenreDetailFragmentDirections.openSongMenu(menu.parcel)
+ is Menu.ForArtist -> GenreDetailFragmentDirections.openArtistMenu(menu.parcel)
+ is Menu.ForGenre -> GenreDetailFragmentDirections.openGenreMenu(menu.parcel)
+ is Menu.ForSelection -> GenreDetailFragmentDirections.openSelectionMenu(menu.parcel)
+ is Menu.ForAlbum,
+ is Menu.ForPlaylist -> error("Unexpected menu $menu")
+ }
+ findNavController().navigateSafe(directions)
+ }
+
private fun updateSelection(selected: List) {
genreListAdapter.setSelected(selected.toSet())
@@ -311,20 +260,19 @@ class GenreDetailFragment :
}
private fun handleDecision(decision: PlaylistDecision?) {
- when (decision) {
- is PlaylistDecision.Add -> {
- logD("Adding ${decision.songs.size} songs to a playlist")
- findNavController()
- .navigateSafe(
- GenreDetailFragmentDirections.addToPlaylist(
- decision.songs.map { it.uid }.toTypedArray()))
- musicModel.playlistDecision.consume()
+ if (decision == null) return
+ val directions =
+ when (decision) {
+ is PlaylistDecision.Add -> {
+ logD("Adding ${decision.songs.size} songs to a playlist")
+ GenreDetailFragmentDirections.addToPlaylist(
+ decision.songs.map { it.uid }.toTypedArray())
+ }
+ is PlaylistDecision.New,
+ is PlaylistDecision.Rename,
+ is PlaylistDecision.Delete -> error("Unexpected playlist decision $decision")
}
- is PlaylistDecision.New,
- is PlaylistDecision.Rename,
- is PlaylistDecision.Delete -> error("Unexpected decision $decision")
- null -> {}
- }
+ findNavController().navigateSafe(directions)
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
@@ -342,15 +290,16 @@ class GenreDetailFragment :
genreListAdapter.setPlaying(playingItem, isPlaying)
}
- private fun handlePlayFromArtist(song: Song?) {
- if (song == null) return
- logD("Launching play from artist dialog for $song")
- findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromArtist(song.uid))
- }
-
- private fun handlePlayFromGenre(song: Song?) {
- if (song == null) return
- logD("Launching play from genre dialog for $song")
- findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromGenre(song.uid))
+ private fun handlePlaybackDecision(decision: PlaybackDecision?) {
+ if (decision == null) return
+ val directions =
+ when (decision) {
+ is PlaybackDecision.PlayFromArtist -> {
+ logD("Launching play from artist dialog for $decision")
+ GenreDetailFragmentDirections.playFromArtist(decision.song.uid)
+ }
+ is PlaybackDecision.PlayFromGenre -> error("Unexpected playback decision $decision")
+ }
+ findNavController().navigateSafe(directions)
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt
index dc13ef831..540017724 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt
@@ -21,10 +21,7 @@ package org.oxycblt.auxio.detail
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
-import android.view.View
import androidx.fragment.app.activityViewModels
-import androidx.navigation.NavController
-import androidx.navigation.NavDestination
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.ConcatAdapter
@@ -43,22 +40,23 @@ import org.oxycblt.auxio.list.Divider
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment
-import org.oxycblt.auxio.list.selection.SelectionViewModel
+import org.oxycblt.auxio.list.ListViewModel
+import org.oxycblt.auxio.list.menu.Menu
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.PlaylistDecision
import org.oxycblt.auxio.music.Song
+import org.oxycblt.auxio.playback.PlaybackDecision
import org.oxycblt.auxio.playback.PlaybackViewModel
+import org.oxycblt.auxio.ui.DialogAwareNavigationListener
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
-import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.navigateSafe
+import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
import org.oxycblt.auxio.util.setFullWidthLookup
-import org.oxycblt.auxio.util.share
-import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
@@ -70,10 +68,9 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
class PlaylistDetailFragment :
ListFragment(),
DetailHeaderAdapter.Listener,
- PlaylistDetailListAdapter.Listener,
- NavController.OnDestinationChangedListener {
- override val detailModel: DetailViewModel by activityViewModels()
- override val selectionModel: SelectionViewModel by activityViewModels()
+ PlaylistDetailListAdapter.Listener {
+ private val detailModel: DetailViewModel by activityViewModels()
+ override val listModel: ListViewModel by activityViewModels()
override val musicModel: MusicViewModel by activityViewModels()
override val playbackModel: PlaybackViewModel by activityViewModels()
// Information about what playlist to display is initially within the navigation arguments
@@ -82,7 +79,7 @@ class PlaylistDetailFragment :
private val playlistHeaderAdapter = PlaylistDetailHeaderAdapter(this)
private val playlistListAdapter = PlaylistDetailListAdapter(this)
private var touchHelper: ItemTouchHelper? = null
- private var initialNavDestinationChange = false
+ private var editNavigationListener: DialogAwareNavigationListener? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -100,11 +97,16 @@ class PlaylistDetailFragment :
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
+ editNavigationListener = DialogAwareNavigationListener(detailModel::dropPlaylistEdit)
+
// --- UI SETUP ---
binding.detailNormalToolbar.apply {
- inflateMenu(R.menu.menu_playlist_detail)
setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener(this@PlaylistDetailFragment)
+ overrideOnOverflowMenuClick {
+ listModel.openMenu(
+ R.menu.detail_playlist, unlikelyToBeNull(detailModel.currentPlaylist.value))
+ }
}
binding.detailEditToolbar.apply {
@@ -121,7 +123,7 @@ class PlaylistDetailFragment :
(layoutManager as GridLayoutManager).setFullWidthLookup {
if (it != 0) {
val item =
- detailModel.playlistList.value.getOrElse(it - 1) {
+ detailModel.playlistSongList.value.getOrElse(it - 1) {
return@setFullWidthLookup false
}
item is Divider || item is Header
@@ -135,28 +137,42 @@ class PlaylistDetailFragment :
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setPlaylist(args.playlistUid)
collectImmediately(detailModel.currentPlaylist, ::updatePlaylist)
- collectImmediately(detailModel.playlistList, ::updateList)
+ collectImmediately(detailModel.playlistSongList, ::updateList)
collectImmediately(detailModel.editedPlaylist, ::updateEditedList)
collect(detailModel.toShow.flow, ::handleShow)
- collectImmediately(selectionModel.selected, ::updateSelection)
+ collect(listModel.menu.flow, ::handleMenu)
+ collectImmediately(listModel.selected, ::updateSelection)
collect(musicModel.playlistDecision.flow, ::handleDecision)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
- collect(playbackModel.artistPickerSong.flow, ::handlePlayFromArtist)
- collect(playbackModel.genrePickerSong.flow, ::handlePlayFromGenre)
+ collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision)
+ }
+
+ override fun onMenuItemClick(item: MenuItem): Boolean {
+ if (super.onMenuItemClick(item)) {
+ return true
+ }
+
+ if (item.itemId == R.id.action_save) {
+ detailModel.savePlaylistEdit()
+ return true
+ }
+
+ return false
}
override fun onStart() {
super.onStart()
// Once we add the destination change callback, we will receive another initialization call,
// so handle that by resetting the flag.
- initialNavDestinationChange = false
- findNavController().addOnDestinationChangedListener(this)
+ requireNotNull(editNavigationListener) { "NavigationListener was not available" }
+ .attach(findNavController())
}
override fun onStop() {
super.onStop()
- findNavController().removeOnDestinationChangedListener(this)
+ requireNotNull(editNavigationListener) { "NavigationListener was not available" }
+ .release(findNavController())
}
override fun onDestroyBinding(binding: FragmentDetailBinding) {
@@ -166,76 +182,20 @@ class PlaylistDetailFragment :
binding.detailRecycler.adapter = null
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
// during list initialization and crash the app. Could happen if the user is fast enough.
- detailModel.playlistInstructions.consume()
- }
-
- override fun onDestinationChanged(
- controller: NavController,
- destination: NavDestination,
- arguments: Bundle?
- ) {
- // Drop the initial call by NavController that simply provides us with the current
- // destination. This would cause the selection state to be lost every time the device
- // rotates.
- if (!initialNavDestinationChange) {
- initialNavDestinationChange = true
- return
- }
- // Drop any pending playlist edits when navigating away. This could actually happen
- // if the user is quick enough.
- detailModel.dropPlaylistEdit()
- }
-
- override fun onMenuItemClick(item: MenuItem): Boolean {
- if (super.onMenuItemClick(item)) {
- return true
- }
-
- val currentPlaylist = unlikelyToBeNull(detailModel.currentPlaylist.value)
- return when (item.itemId) {
- R.id.action_play_next -> {
- playbackModel.playNext(currentPlaylist)
- requireContext().showToast(R.string.lng_queue_added)
- true
- }
- R.id.action_queue_add -> {
- playbackModel.addToQueue(currentPlaylist)
- requireContext().showToast(R.string.lng_queue_added)
- true
- }
- R.id.action_rename -> {
- musicModel.renamePlaylist(currentPlaylist)
- true
- }
- R.id.action_delete -> {
- musicModel.deletePlaylist(currentPlaylist)
- true
- }
- R.id.action_share -> {
- requireContext().share(currentPlaylist)
- true
- }
- R.id.action_save -> {
- detailModel.savePlaylistEdit()
- true
- }
- else -> {
- logW("Unexpected menu item selected")
- false
- }
- }
+ detailModel.playlistSongInstructions.consume()
+ editNavigationListener = null
}
override fun onRealClick(item: Song) {
- playbackModel.playFromPlaylist(item, unlikelyToBeNull(detailModel.currentPlaylist.value))
+ playbackModel.play(item, detailModel.playInPlaylistWith)
}
override fun onPickUp(viewHolder: RecyclerView.ViewHolder) {
requireNotNull(touchHelper) { "ItemTouchHelper was not available" }.startDrag(viewHolder)
}
- override fun onOpenMenu(item: Song, anchor: View) {
- openMusicMenu(anchor, R.menu.menu_playlist_song_actions, item)
+ override fun onOpenMenu(item: Song) {
+ listModel.openMenu(R.menu.playlist_song, item, detailModel.playInPlaylistWith)
}
override fun onPlay() {
@@ -250,7 +210,9 @@ class PlaylistDetailFragment :
detailModel.startPlaylistEdit()
}
- override fun onOpenSortMenu(anchor: View) {}
+ override fun onOpenSortMenu() {
+ findNavController().navigateSafe(PlaylistDetailFragmentDirections.sort())
+ }
private fun updatePlaylist(playlist: Playlist?) {
if (playlist == null) {
@@ -259,30 +221,20 @@ class PlaylistDetailFragment :
return
}
val binding = requireBinding()
- binding.detailNormalToolbar.apply {
- title = playlist.name.resolve(requireContext())
- // Disable options that make no sense with an empty playlist
- val playable = playlist.songs.isNotEmpty()
- if (!playable) {
- logD("Playlist is empty, disabling playback/share options")
- }
- menu.findItem(R.id.action_play_next).isEnabled = playable
- menu.findItem(R.id.action_queue_add).isEnabled = playable
- menu.findItem(R.id.action_share).isEnabled = playable
- }
+ binding.detailNormalToolbar.title = playlist.name.resolve(requireContext())
binding.detailEditToolbar.title =
getString(R.string.fmt_editing, playlist.name.resolve(requireContext()))
playlistHeaderAdapter.setParent(playlist)
}
private fun updateList(list: List
- ) {
- playlistListAdapter.update(list, detailModel.playlistInstructions.consume())
+ playlistListAdapter.update(list, detailModel.playlistSongInstructions.consume())
}
private fun updateEditedList(editedPlaylist: List?) {
playlistListAdapter.setEditing(editedPlaylist != null)
playlistHeaderAdapter.setEditedPlaylist(editedPlaylist)
- selectionModel.drop()
+ listModel.dropSelection()
if (editedPlaylist != null) {
logD("Updating save button state")
@@ -301,38 +253,31 @@ class PlaylistDetailFragment :
findNavController()
.navigateSafe(PlaylistDetailFragmentDirections.showSong(show.song.uid))
}
-
- // Songs should be scrolled to if the album matches, or a new detail
- // fragment should be launched otherwise.
is Show.SongAlbumDetails -> {
logD("Navigating to the album of ${show.song}")
findNavController()
.navigateSafe(PlaylistDetailFragmentDirections.showAlbum(show.song.album.uid))
}
-
- // If the album matches, no need to do anything. Otherwise launch a new
- // detail fragment.
is Show.AlbumDetails -> {
logD("Navigating to ${show.album}")
findNavController()
.navigateSafe(PlaylistDetailFragmentDirections.showAlbum(show.album.uid))
}
-
- // Always launch a new ArtistDetailFragment.
is Show.ArtistDetails -> {
logD("Navigating to ${show.artist}")
findNavController()
.navigateSafe(PlaylistDetailFragmentDirections.showArtist(show.artist.uid))
}
- is Show.SongArtistDetails -> {
+ is Show.SongArtistDecision -> {
logD("Navigating to artist choices for ${show.song}")
findNavController()
- .navigateSafe(PlaylistDetailFragmentDirections.showArtist(show.song.uid))
+ .navigateSafe(PlaylistDetailFragmentDirections.showArtistChoices(show.song.uid))
}
- is Show.AlbumArtistDetails -> {
+ is Show.AlbumArtistDecision -> {
logD("Navigating to artist choices for ${show.album}")
findNavController()
- .navigateSafe(PlaylistDetailFragmentDirections.showArtist(show.album.uid))
+ .navigateSafe(
+ PlaylistDetailFragmentDirections.showArtistChoices(show.album.uid))
}
is Show.PlaylistDetails -> {
logD("Navigated to this playlist")
@@ -345,6 +290,22 @@ class PlaylistDetailFragment :
}
}
+ private fun handleMenu(menu: Menu?) {
+ if (menu == null) return
+ val directions =
+ when (menu) {
+ is Menu.ForSong -> PlaylistDetailFragmentDirections.openSongMenu(menu.parcel)
+ is Menu.ForPlaylist ->
+ PlaylistDetailFragmentDirections.openPlaylistMenu(menu.parcel)
+ is Menu.ForSelection ->
+ PlaylistDetailFragmentDirections.openSelectionMenu(menu.parcel)
+ is Menu.ForArtist,
+ is Menu.ForAlbum,
+ is Menu.ForGenre -> error("Unexpected menu $menu")
+ }
+ findNavController().navigateSafe(directions)
+ }
+
private fun updateSelection(selected: List) {
playlistListAdapter.setSelected(selected.toSet())
@@ -357,23 +318,24 @@ class PlaylistDetailFragment :
private fun handleDecision(decision: PlaylistDecision?) {
if (decision == null) return
- when (decision) {
- is PlaylistDecision.Rename -> {
- logD("Renaming ${decision.playlist}")
- findNavController()
- .navigateSafe(
- PlaylistDetailFragmentDirections.renamePlaylist(decision.playlist.uid))
+ val directions =
+ when (decision) {
+ is PlaylistDecision.Rename -> {
+ logD("Renaming ${decision.playlist}")
+ PlaylistDetailFragmentDirections.renamePlaylist(decision.playlist.uid)
+ }
+ is PlaylistDecision.Delete -> {
+ logD("Deleting ${decision.playlist}")
+ PlaylistDetailFragmentDirections.deletePlaylist(decision.playlist.uid)
+ }
+ is PlaylistDecision.Add -> {
+ logD("Adding ${decision.songs.size} songs to a playlist")
+ PlaylistDetailFragmentDirections.addToPlaylist(
+ decision.songs.map { it.uid }.toTypedArray())
+ }
+ is PlaylistDecision.New -> error("Unexpected playlist decision $decision")
}
- is PlaylistDecision.Delete -> {
- logD("Deleting ${decision.playlist}")
- findNavController()
- .navigateSafe(
- PlaylistDetailFragmentDirections.deletePlaylist(decision.playlist.uid))
- }
- is PlaylistDecision.Add,
- is PlaylistDecision.New -> error("Unexpected decision $decision")
- }
- musicModel.playlistDecision.consume()
+ findNavController().navigateSafe(directions)
}
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
@@ -382,17 +344,22 @@ class PlaylistDetailFragment :
song.takeIf { parent == detailModel.currentPlaylist.value }, isPlaying)
}
- private fun handlePlayFromArtist(song: Song?) {
- if (song == null) return
- logD("Launching play from artist dialog for $song")
- findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromArtist(song.uid))
+ private fun handlePlaybackDecision(decision: PlaybackDecision?) {
+ if (decision == null) return
+ val directions =
+ when (decision) {
+ is PlaybackDecision.PlayFromArtist -> {
+ logD("Launching play from artist dialog for $decision")
+ PlaylistDetailFragmentDirections.playFromArtist(decision.song.uid)
+ }
+ is PlaybackDecision.PlayFromGenre -> {
+ logD("Launching play from artist dialog for $decision")
+ PlaylistDetailFragmentDirections.playFromGenre(decision.song.uid)
+ }
+ }
+ findNavController().navigateSafe(directions)
}
- private fun handlePlayFromGenre(song: Song?) {
- if (song == null) return
- logD("Launching play from genre dialog for $song")
- findNavController().navigateSafe(AlbumDetailFragmentDirections.playFromGenre(song.uid))
- }
private fun updateMultiToolbar() {
val id =
when {
@@ -400,7 +367,7 @@ class PlaylistDetailFragment :
logD("Currently editing playlist, showing edit toolbar")
R.id.detail_edit_toolbar
}
- selectionModel.selected.value.isNotEmpty() -> {
+ listModel.selected.value.isNotEmpty() -> {
logD("Currently selecting, showing selection toolbar")
R.id.detail_selection_toolbar
}
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt
index f7b293a06..f43da103c 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt
@@ -38,18 +38,18 @@ import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.metadata.AudioProperties
import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.playback.formatDurationMs
-import org.oxycblt.auxio.ui.ViewBindingDialogFragment
+import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.concatLocalized
import org.oxycblt.auxio.util.logD
/**
- * A [ViewBindingDialogFragment] that shows information about a Song.
+ * A [ViewBindingMaterialDialogFragment] that shows information about a Song.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
-class SongDetailDialog : ViewBindingDialogFragment() {
+class SongDetailDialog : ViewBindingMaterialDialogFragment() {
private val detailModel: DetailViewModel by activityViewModels()
// Information about what song to display is initially within the navigation arguments
// as a UID, as that is the only safe way to parcel an song.
@@ -69,8 +69,8 @@ class SongDetailDialog : ViewBindingDialogFragment() {
binding.detailProperties.adapter = detailAdapter
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setSong(args.songUid)
+ detailModel.toShow.consume()
collectImmediately(detailModel.currentSong, detailModel.songAudioProperties, ::updateSong)
- collectImmediately(detailModel.toShow.flow, ::handleShow)
}
private fun updateSong(song: Song?, info: AudioProperties?) {
@@ -126,16 +126,6 @@ class SongDetailDialog : ViewBindingDialogFragment() {
}
}
- private fun handleShow(show: Show?) {
- if (show == null) return
- if (show is Show.SongDetails) {
- logD("Navigated to this song")
- detailModel.toShow.consume()
- } else {
- error("Unexpected show command $show")
- }
- }
-
private fun T.zipName(context: Context): String {
val name = name
return if (name is Name.Known && name.sort != null) {
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/picker/ArtistShowChoice.kt b/app/src/main/java/org/oxycblt/auxio/detail/decision/ArtistShowChoice.kt
similarity index 98%
rename from app/src/main/java/org/oxycblt/auxio/detail/picker/ArtistShowChoice.kt
rename to app/src/main/java/org/oxycblt/auxio/detail/decision/ArtistShowChoice.kt
index f28b9d765..98a411a04 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/picker/ArtistShowChoice.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/decision/ArtistShowChoice.kt
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package org.oxycblt.auxio.detail.picker
+package org.oxycblt.auxio.detail.decision
import android.view.View
import android.view.ViewGroup
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/picker/NavigationPickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt
similarity index 91%
rename from app/src/main/java/org/oxycblt/auxio/detail/picker/NavigationPickerViewModel.kt
rename to app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt
index a510b6e4a..efe219235 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/picker/NavigationPickerViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2023 Auxio Project
- * NavigationPickerViewModel.kt is part of Auxio.
+ * DetailDecisionViewModel.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package org.oxycblt.auxio.detail.picker
+package org.oxycblt.auxio.detail.decision
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -33,12 +33,13 @@ import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
/**
- * A [ViewModel] that stores the current information required for navigation picker dialogs
+ * A [ViewModel] that stores choice information for [ShowArtistDialog], and possibly others in the
+ * future.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@HiltViewModel
-class NavigationPickerViewModel @Inject constructor(private val musicRepository: MusicRepository) :
+class DetailPickerViewModel @Inject constructor(private val musicRepository: MusicRepository) :
ViewModel(), MusicRepository.UpdateListener {
private val _artistChoices = MutableStateFlow(null)
/** The current set of [Artist] choices to show in the picker, or null if to show nothing. */
@@ -49,6 +50,11 @@ class NavigationPickerViewModel @Inject constructor(private val musicRepository:
musicRepository.addUpdateListener(this)
}
+ override fun onCleared() {
+ super.onCleared()
+ musicRepository.removeUpdateListener(this)
+ }
+
override fun onMusicChanges(changes: MusicRepository.Changes) {
if (!changes.deviceLibrary) return
val deviceLibrary = musicRepository.deviceLibrary ?: return
@@ -57,11 +63,6 @@ class NavigationPickerViewModel @Inject constructor(private val musicRepository:
logD("Updated artist choices: ${_artistChoices.value}")
}
- override fun onCleared() {
- super.onCleared()
- musicRepository.removeUpdateListener(this)
- }
-
/**
* Set the [Music.UID] of the item to show artist choices for.
*
@@ -105,16 +106,16 @@ sealed interface ArtistShowChoices {
class FromSong(val song: Song) : ArtistShowChoices {
override val uid = song.uid
override val choices = song.artists
+
override fun sanitize(newLibrary: DeviceLibrary) =
newLibrary.findSong(uid)?.let { FromSong(it) }
}
- /**
- * Backing implementation of [ArtistShowChoices] that is based on an [AlbumArtistShowChoices].
- */
+ /** Backing implementation of [ArtistShowChoices] that is based on an [Album]. */
data class FromAlbum(val album: Album) : ArtistShowChoices {
override val uid = album.uid
override val choices = album.artists
+
override fun sanitize(newLibrary: DeviceLibrary) =
newLibrary.findAlbum(uid)?.let { FromAlbum(it) }
}
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/picker/ShowArtistDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/decision/ShowArtistDialog.kt
similarity index 78%
rename from app/src/main/java/org/oxycblt/auxio/detail/picker/ShowArtistDialog.kt
rename to app/src/main/java/org/oxycblt/auxio/detail/decision/ShowArtistDialog.kt
index a98dfb162..1f82ddfe2 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/picker/ShowArtistDialog.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/decision/ShowArtistDialog.kt
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package org.oxycblt.auxio.detail.picker
+package org.oxycblt.auxio.detail.decision
import android.os.Bundle
import android.view.LayoutInflater
@@ -33,19 +33,20 @@ import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.music.Artist
-import org.oxycblt.auxio.ui.ViewBindingDialogFragment
+import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
import org.oxycblt.auxio.util.collectImmediately
+import org.oxycblt.auxio.util.logD
/**
- * A picker [ViewBindingDialogFragment] intended for when the [Artist] to show is ambiguous.
+ * A picker [ViewBindingMaterialDialogFragment] intended for when the [Artist] to show is ambiguous.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class ShowArtistDialog :
- ViewBindingDialogFragment(), ClickableListListener {
+ ViewBindingMaterialDialogFragment(), ClickableListListener {
private val detailModel: DetailViewModel by activityViewModels()
- private val pickerModel: NavigationPickerViewModel by viewModels()
+ private val pickerModel: DetailPickerViewModel by viewModels()
// Information about what artists to show choices for is initially within the navigation
// arguments as UIDs, as that is the only safe way to parcel an artist.
private val args: ShowArtistDialogArgs by navArgs()
@@ -66,14 +67,9 @@ class ShowArtistDialog :
adapter = choiceAdapter
}
+ detailModel.toShow.consume()
pickerModel.setArtistChoiceUid(args.itemUid)
- collectImmediately(pickerModel.artistChoices) {
- if (it != null) {
- choiceAdapter.update(it.choices, UpdateInstructions.Replace(0))
- } else {
- findNavController().navigateUp()
- }
- }
+ collectImmediately(pickerModel.artistChoices, ::updateChoices)
}
override fun onDestroyBinding(binding: DialogMusicChoicesBinding) {
@@ -82,8 +78,17 @@ class ShowArtistDialog :
}
override fun onClick(item: Artist, viewHolder: RecyclerView.ViewHolder) {
+ findNavController().navigateUp()
// User made a choice, navigate to the artist.
detailModel.showArtist(item)
- findNavController().navigateUp()
+ }
+
+ private fun updateChoices(choices: ArtistShowChoices?) {
+ if (choices == null) {
+ logD("No choices to show, navigating away")
+ findNavController().navigateUp()
+ return
+ }
+ choiceAdapter.update(choices.choices, UpdateInstructions.Diff)
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt
index 02303a566..e85c892e7 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/header/ArtistDetailHeaderAdapter.kt
@@ -41,6 +41,7 @@ class ArtistDetailHeaderAdapter(private val listener: Listener) :
DetailHeaderAdapter() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ArtistDetailHeaderViewHolder.from(parent)
+
override fun onBindHeader(holder: ArtistDetailHeaderViewHolder, parent: Artist) =
holder.bind(parent, listener)
}
@@ -70,7 +71,11 @@ private constructor(private val binding: ItemDetailHeaderBinding) :
binding.detailInfo.text =
binding.context.getString(
R.string.fmt_two,
- binding.context.getPlural(R.plurals.fmt_album_count, artist.albums.size),
+ if (artist.explicitAlbums.isNotEmpty()) {
+ binding.context.getPlural(R.plurals.fmt_album_count, artist.explicitAlbums.size)
+ } else {
+ binding.context.getString(R.string.def_album_count)
+ },
if (artist.songs.isNotEmpty()) {
binding.context.getPlural(R.plurals.fmt_song_count, artist.songs.size)
} else {
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt
index 247875432..4afabb6c3 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt
@@ -30,7 +30,9 @@ import org.oxycblt.auxio.util.logD
abstract class DetailHeaderAdapter :
RecyclerView.Adapter() {
private var currentParent: T? = null
+
final override fun getItemCount() = 1
+
final override fun onBindViewHolder(holder: VH, position: Int) =
onBindHeader(holder, requireNotNull(currentParent))
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt
index ba350e7b3..08293199f 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt
@@ -82,7 +82,7 @@ abstract class DetailListAdapter(
* Called when the button in a [SortHeader] item is pressed, requesting that the sort menu
* should be opened.
*/
- fun onOpenSortMenu(anchor: View)
+ fun onOpenSortMenu()
}
protected companion object {
@@ -132,7 +132,7 @@ private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
// Add a Tooltip based on the content description so that the purpose of this
// button can be clear.
TooltipCompat.setTooltipText(this, contentDescription)
- setOnClickListener(listener::onOpenSortMenu)
+ setOnClickListener { listener.onOpenSortMenu() }
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt
index 06c5be29b..ca8a0657b 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt
@@ -170,10 +170,25 @@ private class EditHeaderViewHolder private constructor(private val binding: Item
TooltipCompat.setTooltipText(this, contentDescription)
setOnClickListener { listener.onStartEdit() }
}
+ binding.headerSort.apply {
+ TooltipCompat.setTooltipText(this, contentDescription)
+ setOnClickListener { listener.onOpenSortMenu() }
+ }
}
override fun updateEditing(editing: Boolean) {
- binding.headerEdit.isEnabled = !editing
+ binding.headerEdit.apply {
+ isVisible = !editing
+ isClickable = !editing
+ isFocusable = !editing
+ jumpDrawablesToCurrentState()
+ }
+ binding.headerSort.apply {
+ isVisible = editing
+ isClickable = editing
+ isFocusable = editing
+ jumpDrawablesToCurrentState()
+ }
}
companion object {
@@ -211,6 +226,7 @@ private constructor(private val binding: ItemEditableSongBinding) :
PlaylistDetailListAdapter.ViewHolder {
override val enabled: Boolean
get() = binding.songDragHandle.isVisible
+
override val root = binding.root
override val body = binding.body
override val delete = binding.background
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/sort/AlbumSongSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/sort/AlbumSongSortDialog.kt
new file mode 100644
index 000000000..de8fe127d
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/detail/sort/AlbumSongSortDialog.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ * AlbumSongSortDialog.kt is part of Auxio.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.auxio.detail.sort
+
+import android.os.Bundle
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.fragment.findNavController
+import dagger.hilt.android.AndroidEntryPoint
+import org.oxycblt.auxio.databinding.DialogSortBinding
+import org.oxycblt.auxio.detail.DetailViewModel
+import org.oxycblt.auxio.list.sort.Sort
+import org.oxycblt.auxio.list.sort.SortDialog
+import org.oxycblt.auxio.music.Album
+import org.oxycblt.auxio.util.collectImmediately
+import org.oxycblt.auxio.util.logD
+
+/**
+ * A [SortDialog] that controls the [Sort] of [DetailViewModel.albumSongSort].
+ *
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+@AndroidEntryPoint
+class AlbumSongSortDialog : SortDialog() {
+ private val detailModel: DetailViewModel by activityViewModels()
+
+ override fun onBindingCreated(binding: DialogSortBinding, savedInstanceState: Bundle?) {
+ super.onBindingCreated(binding, savedInstanceState)
+
+ // --- VIEWMODEL SETUP ---
+ collectImmediately(detailModel.currentAlbum, ::updateAlbum)
+ }
+
+ override fun getInitialSort() = detailModel.albumSongSort
+
+ override fun applyChosenSort(sort: Sort) {
+ detailModel.applyAlbumSongSort(sort)
+ }
+
+ override fun getModeChoices() = listOf(Sort.Mode.ByDisc, Sort.Mode.ByTrack)
+
+ private fun updateAlbum(album: Album?) {
+ if (album == null) {
+ logD("No album to sort, navigating away")
+ findNavController().navigateUp()
+ }
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/sort/ArtistSongSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/sort/ArtistSongSortDialog.kt
new file mode 100644
index 000000000..d0d4e355a
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/detail/sort/ArtistSongSortDialog.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ * ArtistSongSortDialog.kt is part of Auxio.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.auxio.detail.sort
+
+import android.os.Bundle
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.fragment.findNavController
+import dagger.hilt.android.AndroidEntryPoint
+import org.oxycblt.auxio.databinding.DialogSortBinding
+import org.oxycblt.auxio.detail.DetailViewModel
+import org.oxycblt.auxio.list.sort.Sort
+import org.oxycblt.auxio.list.sort.SortDialog
+import org.oxycblt.auxio.music.Artist
+import org.oxycblt.auxio.util.collectImmediately
+import org.oxycblt.auxio.util.logD
+
+/**
+ * A [SortDialog] that controls the [Sort] of [DetailViewModel.artistSongSort].
+ *
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+@AndroidEntryPoint
+class ArtistSongSortDialog : SortDialog() {
+ private val detailModel: DetailViewModel by activityViewModels()
+
+ override fun onBindingCreated(binding: DialogSortBinding, savedInstanceState: Bundle?) {
+ super.onBindingCreated(binding, savedInstanceState)
+
+ // --- VIEWMODEL SETUP ---
+ collectImmediately(detailModel.currentArtist, ::updateArtist)
+ }
+
+ override fun getInitialSort() = detailModel.artistSongSort
+
+ override fun applyChosenSort(sort: Sort) {
+ detailModel.applyArtistSongSort(sort)
+ }
+
+ override fun getModeChoices() =
+ listOf(Sort.Mode.ByName, Sort.Mode.ByAlbum, Sort.Mode.ByDate, Sort.Mode.ByDuration)
+
+ private fun updateArtist(artist: Artist?) {
+ if (artist == null) {
+ logD("No artist to sort, navigating away")
+ findNavController().navigateUp()
+ }
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/sort/GenreSongSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/sort/GenreSongSortDialog.kt
new file mode 100644
index 000000000..88c69172b
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/detail/sort/GenreSongSortDialog.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ * GenreSongSortDialog.kt is part of Auxio.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.auxio.detail.sort
+
+import android.os.Bundle
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.fragment.findNavController
+import dagger.hilt.android.AndroidEntryPoint
+import org.oxycblt.auxio.databinding.DialogSortBinding
+import org.oxycblt.auxio.detail.DetailViewModel
+import org.oxycblt.auxio.list.sort.Sort
+import org.oxycblt.auxio.list.sort.SortDialog
+import org.oxycblt.auxio.music.Genre
+import org.oxycblt.auxio.util.collectImmediately
+import org.oxycblt.auxio.util.logD
+
+/**
+ * A [SortDialog] that controls the [Sort] of [DetailViewModel.genreSongSort].
+ *
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+@AndroidEntryPoint
+class GenreSongSortDialog : SortDialog() {
+ private val detailModel: DetailViewModel by activityViewModels()
+
+ override fun onBindingCreated(binding: DialogSortBinding, savedInstanceState: Bundle?) {
+ super.onBindingCreated(binding, savedInstanceState)
+
+ // --- VIEWMODEL SETUP ---
+ collectImmediately(detailModel.currentGenre, ::updateGenre)
+ }
+
+ override fun getInitialSort() = detailModel.genreSongSort
+
+ override fun applyChosenSort(sort: Sort) {
+ detailModel.applyGenreSongSort(sort)
+ }
+
+ override fun getModeChoices() =
+ listOf(
+ Sort.Mode.ByName,
+ Sort.Mode.ByArtist,
+ Sort.Mode.ByAlbum,
+ Sort.Mode.ByDate,
+ Sort.Mode.ByDuration)
+
+ private fun updateGenre(genre: Genre?) {
+ if (genre == null) {
+ logD("No genre to sort, navigating away")
+ findNavController().navigateUp()
+ }
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/sort/PlaylistSongSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/sort/PlaylistSongSortDialog.kt
new file mode 100644
index 000000000..923d41829
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/detail/sort/PlaylistSongSortDialog.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ * PlaylistSongSortDialog.kt is part of Auxio.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.auxio.detail.sort
+
+import android.os.Bundle
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.fragment.findNavController
+import dagger.hilt.android.AndroidEntryPoint
+import org.oxycblt.auxio.databinding.DialogSortBinding
+import org.oxycblt.auxio.detail.DetailViewModel
+import org.oxycblt.auxio.list.sort.Sort
+import org.oxycblt.auxio.list.sort.SortDialog
+import org.oxycblt.auxio.music.Playlist
+import org.oxycblt.auxio.util.collectImmediately
+import org.oxycblt.auxio.util.logD
+
+/**
+ * A [SortDialog] that controls the [Sort] of [DetailViewModel.genreSongSort].
+ *
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+@AndroidEntryPoint
+class PlaylistSongSortDialog : SortDialog() {
+ private val detailModel: DetailViewModel by activityViewModels()
+
+ override fun onBindingCreated(binding: DialogSortBinding, savedInstanceState: Bundle?) {
+ super.onBindingCreated(binding, savedInstanceState)
+
+ // --- VIEWMODEL SETUP ---
+ collectImmediately(detailModel.currentPlaylist, ::updatePlaylist)
+ }
+
+ override fun getInitialSort() = null
+
+ override fun applyChosenSort(sort: Sort) {
+ detailModel.applyPlaylistSongSort(sort)
+ }
+
+ override fun getModeChoices() =
+ listOf(
+ Sort.Mode.ByName,
+ Sort.Mode.ByArtist,
+ Sort.Mode.ByAlbum,
+ Sort.Mode.ByDate,
+ Sort.Mode.ByDuration)
+
+ private fun updatePlaylist(genre: Playlist?) {
+ if (genre == null) {
+ logD("No genre to sort, navigating away")
+ findNavController().navigateUp()
+ }
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/ErrorDetailsDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/ErrorDetailsDialog.kt
new file mode 100644
index 000000000..e88ed1175
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/home/ErrorDetailsDialog.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ * ErrorDetailsDialog.kt is part of Auxio.
+ *
+ * This 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.ClipData
+import android.content.ClipboardManager
+import android.os.Build
+import android.os.Bundle
+import android.view.LayoutInflater
+import androidx.appcompat.app.AlertDialog
+import androidx.navigation.fragment.navArgs
+import org.oxycblt.auxio.R
+import org.oxycblt.auxio.databinding.DialogErrorDetailsBinding
+import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
+import org.oxycblt.auxio.util.getSystemServiceCompat
+import org.oxycblt.auxio.util.openInBrowser
+import org.oxycblt.auxio.util.showToast
+
+/**
+ * A dialog that shows a stack trace for a music loading error.
+ *
+ * @author Alexander Capehart (OxygenCobalt)
+ *
+ * TODO: Extend to other errors
+ */
+class ErrorDetailsDialog : ViewBindingMaterialDialogFragment() {
+ private val args: ErrorDetailsDialogArgs by navArgs()
+ private var clipboardManager: ClipboardManager? = null
+
+ override fun onConfigDialog(builder: AlertDialog.Builder) {
+ builder
+ .setTitle(R.string.lbl_error_info)
+ .setPositiveButton(R.string.lbl_report) { _, _ ->
+ requireContext().openInBrowser(LINK_ISSUES)
+ }
+ .setNegativeButton(R.string.lbl_cancel, null)
+ }
+
+ override fun onCreateBinding(inflater: LayoutInflater) =
+ DialogErrorDetailsBinding.inflate(inflater)
+
+ override fun onBindingCreated(binding: DialogErrorDetailsBinding, savedInstanceState: Bundle?) {
+ super.onBindingCreated(binding, savedInstanceState)
+
+ clipboardManager = requireContext().getSystemServiceCompat(ClipboardManager::class)
+
+ // --- UI SETUP ---
+ binding.errorStackTrace.text = args.error.stackTraceToString().trimEnd('\n')
+ binding.errorCopy.setOnClickListener { copyStackTrace() }
+ }
+
+ override fun onDestroyBinding(binding: DialogErrorDetailsBinding) {
+ super.onDestroyBinding(binding)
+ clipboardManager = null
+ }
+
+ private fun copyStackTrace() {
+ requireNotNull(clipboardManager) { "Clipboard was unavailable" }
+ .setPrimaryClip(
+ ClipData.newPlainText("Exception Stack Trace", args.error.stackTraceToString()))
+ // A copy notice is shown by the system from Android 13 onwards
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ requireContext().showToast(R.string.lbl_copied)
+ }
+ }
+
+ private companion object {
+ /** The URL to the bug report issue form */
+ const val LINK_ISSUES =
+ "https://github.com/OxygenCobalt/Auxio/issues/new" +
+ "?assignees=OxygenCobalt&labels=bug&projects=&template=bug-crash-report.yml"
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt b/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt
index a03adccfd..c3cd4a82f 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/FlipFloatingActionButton.kt
@@ -51,7 +51,7 @@ constructor(
// Apply the new configuration possibly set in flipTo. This should occur even if
// a flip was canceled by a hide.
pendingConfig?.run {
- this@FlipFloatingActionButton.logD("Applying pending configuration")
+ logD("Applying pending configuration")
setImageResource(iconRes)
contentDescription = context.getString(contentDescriptionRes)
setOnClickListener(clickListener)
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 7aa4dbe5f..01558611d 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
@@ -26,7 +26,6 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.MenuCompat
import androidx.core.view.isVisible
-import androidx.core.view.iterator
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
@@ -54,13 +53,13 @@ import org.oxycblt.auxio.home.list.PlaylistListFragment
import org.oxycblt.auxio.home.list.SongListFragment
import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy
import org.oxycblt.auxio.home.tabs.Tab
-import org.oxycblt.auxio.list.Sort
-import org.oxycblt.auxio.list.selection.SelectionFragment
-import org.oxycblt.auxio.list.selection.SelectionViewModel
+import org.oxycblt.auxio.list.ListViewModel
+import org.oxycblt.auxio.list.SelectionFragment
+import org.oxycblt.auxio.list.menu.Menu
import org.oxycblt.auxio.music.IndexingProgress
import org.oxycblt.auxio.music.IndexingState
import org.oxycblt.auxio.music.Music
-import org.oxycblt.auxio.music.MusicMode
+import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.NoAudioPermissionException
import org.oxycblt.auxio.music.NoMusicException
@@ -75,7 +74,6 @@ import org.oxycblt.auxio.util.lazyReflectedField
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.navigateSafe
-import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* The starting [SelectionFragment] of Auxio. Shows the user's music library and enables navigation
@@ -86,7 +84,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
@AndroidEntryPoint
class HomeFragment :
SelectionFragment(), AppBarLayout.OnOffsetChangedListener {
- override val selectionModel: SelectionViewModel by activityViewModels()
+ override val listModel: ListViewModel by activityViewModels()
override val musicModel: MusicViewModel by activityViewModels()
override val playbackModel: PlaybackViewModel by activityViewModels()
private val homeModel: HomeViewModel by activityViewModels()
@@ -100,9 +98,9 @@ class HomeFragment :
// Orientation change will wipe whatever transition we were using prior, which will
// result in no transition when the user navigates back. Make sure we re-initialize
// our transitions.
- val axis = savedInstanceState.getInt(KEY_LAST_TRANSITION_AXIS, -1)
+ val axis = savedInstanceState.getInt(KEY_LAST_TRANSITION_ID, -1)
if (axis > -1) {
- setupAxisTransitions(axis)
+ applyAxisTransition(axis)
}
}
}
@@ -170,18 +168,19 @@ class HomeFragment :
// --- VIEWMODEL SETUP ---
collect(homeModel.recreateTabs.flow, ::handleRecreate)
- collectImmediately(homeModel.currentTabMode, ::updateCurrentTab)
- collectImmediately(homeModel.songsList, homeModel.isFastScrolling, ::updateFab)
- collectImmediately(selectionModel.selected, ::updateSelection)
+ collectImmediately(homeModel.currentTabType, ::updateCurrentTab)
+ collectImmediately(homeModel.songList, homeModel.isFastScrolling, ::updateFab)
+ collect(listModel.menu.flow, ::handleMenu)
+ collectImmediately(listModel.selected, ::updateSelection)
collectImmediately(musicModel.indexingState, ::updateIndexerState)
collect(musicModel.playlistDecision.flow, ::handleDecision)
collect(detailModel.toShow.flow, ::handleShow)
}
override fun onSaveInstanceState(outState: Bundle) {
- val enter = enterTransition
- if (enter is MaterialSharedAxis) {
- outState.putInt(KEY_LAST_TRANSITION_AXIS, enter.axis)
+ val transition = enterTransition
+ if (transition is MaterialSharedAxis) {
+ outState.putInt(KEY_LAST_TRANSITION_ID, transition.axis)
}
super.onSaveInstanceState(outState)
@@ -214,67 +213,48 @@ class HomeFragment :
// Handle main actions (Search, Settings, About)
R.id.action_search -> {
logD("Navigating to search")
- setupAxisTransitions(MaterialSharedAxis.Z)
+ applyAxisTransition(MaterialSharedAxis.Z)
findNavController().navigateSafe(HomeFragmentDirections.search())
true
}
R.id.action_settings -> {
logD("Navigating to preferences")
- findNavController().navigateSafe(HomeFragmentDirections.preferences())
+ homeModel.showSettings()
true
}
R.id.action_about -> {
logD("Navigating to about")
- findNavController().navigateSafe(HomeFragmentDirections.about())
+ homeModel.showAbout()
true
}
// Handle sort menu
- R.id.submenu_sorting -> {
+ R.id.action_sort -> {
// Junk click event when opening the menu
- true
- }
- R.id.option_sort_asc -> {
- logD("Switching to ascending sorting")
- item.isChecked = true
- homeModel.setSortForCurrentTab(
- homeModel
- .getSortForTab(homeModel.currentTabMode.value)
- .withDirection(Sort.Direction.ASCENDING))
- true
- }
- R.id.option_sort_dec -> {
- logD("Switching to descending sorting")
- item.isChecked = true
- homeModel.setSortForCurrentTab(
- homeModel
- .getSortForTab(homeModel.currentTabMode.value)
- .withDirection(Sort.Direction.DESCENDING))
+ val directions =
+ when (homeModel.currentTabType.value) {
+ MusicType.SONGS -> HomeFragmentDirections.sortSongs()
+ MusicType.ALBUMS -> HomeFragmentDirections.sortAlbums()
+ MusicType.ARTISTS -> HomeFragmentDirections.sortArtists()
+ MusicType.GENRES -> HomeFragmentDirections.sortGenres()
+ MusicType.PLAYLISTS -> HomeFragmentDirections.sortPlaylists()
+ }
+ findNavController().navigateSafe(directions)
true
}
else -> {
- val newMode = Sort.Mode.fromItemId(item.itemId)
- if (newMode != null) {
- // Sorting option was selected, mark it as selected and update the mode
- logD("Updating sort mode")
- item.isChecked = true
- homeModel.setSortForCurrentTab(
- homeModel.getSortForTab(homeModel.currentTabMode.value).withMode(newMode))
- true
- } else {
- logW("Unexpected menu item selected")
- false
- }
+ logW("Unexpected menu item selected")
+ false
}
}
}
private fun setupPager(binding: FragmentHomeBinding) {
binding.homePager.adapter =
- HomePagerAdapter(homeModel.currentTabModes, childFragmentManager, viewLifecycleOwner)
+ HomePagerAdapter(homeModel.currentTabTypes, childFragmentManager, viewLifecycleOwner)
val toolbarParams = binding.homeToolbar.layoutParams as AppBarLayout.LayoutParams
- if (homeModel.currentTabModes.size == 1) {
+ if (homeModel.currentTabTypes.size == 1) {
// A single tab makes the tab layout redundant, hide it and disable the collapsing
// behavior.
logD("Single tab shown, disabling TabLayout")
@@ -292,81 +272,26 @@ class HomeFragment :
TabLayoutMediator(
binding.homeTabs,
binding.homePager,
- AdaptiveTabStrategy(requireContext(), homeModel.currentTabModes))
+ AdaptiveTabStrategy(requireContext(), homeModel.currentTabTypes))
.attach()
}
- private fun updateCurrentTab(tabMode: MusicMode) {
+ private fun updateCurrentTab(tabType: MusicType) {
val binding = requireBinding()
- // Update the sort options to align with those allowed by the tab
- val isVisible: (Int) -> Boolean =
- when (tabMode) {
- // Disallow sorting by count for songs
- MusicMode.SONGS -> {
- logD("Using song-specific menu options")
- ({ id -> id != R.id.option_sort_count })
- }
- // Disallow sorting by album for albums
- MusicMode.ALBUMS -> {
- logD("Using album-specific menu options")
- ({ id -> id != R.id.option_sort_album })
- }
- // Only allow sorting by name, count, and duration for parents
- else -> {
- logD("Using parent-specific menu options")
- ({ id ->
- id == R.id.option_sort_asc ||
- id == R.id.option_sort_dec ||
- id == R.id.option_sort_name ||
- id == R.id.option_sort_count ||
- id == R.id.option_sort_duration
- })
- }
- }
-
- val sortMenu =
- unlikelyToBeNull(binding.homeNormalToolbar.menu.findItem(R.id.submenu_sorting).subMenu)
- val toHighlight = homeModel.getSortForTab(tabMode)
-
- for (option in sortMenu) {
- val isCurrentMode = option.itemId == toHighlight.mode.itemId
- val isCurrentlyAscending =
- option.itemId == R.id.option_sort_asc &&
- toHighlight.direction == Sort.Direction.ASCENDING
- val isCurrentlyDescending =
- option.itemId == R.id.option_sort_dec &&
- toHighlight.direction == Sort.Direction.DESCENDING
- // Check the corresponding direction and mode sort options to align with
- // the current sort of the tab.
- if (isCurrentMode || isCurrentlyAscending || isCurrentlyDescending) {
- logD(
- "Checking $option option [mode: $isCurrentMode asc: $isCurrentlyAscending dec: $isCurrentlyDescending]")
- // Note: We cannot inline this boolean assignment since it unchecks all other radio
- // buttons (even when setting it to false), which would result in nothing being
- // selected.
- option.isChecked = true
- }
-
- // Disable options that are not allowed by the isVisible lambda
- option.isVisible = isVisible(option.itemId)
- if (!option.isVisible) {
- logD("Hiding $option option")
- }
- }
// Update the scrolling view in AppBarLayout to align with the current tab's
// scrolling state. This prevents the lift state from being confused as one
// goes between different tabs.
binding.homeAppbar.liftOnScrollTargetViewId =
- when (tabMode) {
- MusicMode.SONGS -> R.id.home_song_recycler
- MusicMode.ALBUMS -> R.id.home_album_recycler
- MusicMode.ARTISTS -> R.id.home_artist_recycler
- MusicMode.GENRES -> R.id.home_genre_recycler
- MusicMode.PLAYLISTS -> R.id.home_playlist_recycler
+ when (tabType) {
+ MusicType.SONGS -> R.id.home_song_recycler
+ MusicType.ALBUMS -> R.id.home_album_recycler
+ MusicType.ARTISTS -> R.id.home_artist_recycler
+ MusicType.GENRES -> R.id.home_genre_recycler
+ MusicType.PLAYLISTS -> R.id.home_playlist_recycler
}
- if (tabMode != MusicMode.PLAYLISTS) {
+ if (tabType != MusicType.PLAYLISTS) {
logD("Flipping to shuffle button")
binding.homeFab.flipTo(R.drawable.ic_shuffle_off_24, R.string.desc_shuffle_all) {
playbackModel.shuffleAll()
@@ -405,7 +330,7 @@ class HomeFragment :
}
}
- private fun setupCompleteState(binding: FragmentHomeBinding, error: Throwable?) {
+ private fun setupCompleteState(binding: FragmentHomeBinding, error: Exception?) {
if (error == null) {
logD("Received ok response")
binding.homeFab.show()
@@ -417,13 +342,13 @@ class HomeFragment :
val context = requireContext()
binding.homeIndexingContainer.visibility = View.VISIBLE
binding.homeIndexingProgress.visibility = View.INVISIBLE
+ binding.homeIndexingActions.visibility = View.VISIBLE
when (error) {
is NoAudioPermissionException -> {
logD("Showing permission prompt")
binding.homeIndexingStatus.text = context.getString(R.string.err_no_perms)
// Configure the action to act as a permission launcher.
- binding.homeIndexingAction.apply {
- visibility = View.VISIBLE
+ binding.homeIndexingTry.apply {
text = context.getString(R.string.lbl_grant)
setOnClickListener {
requireNotNull(storagePermissionLauncher) {
@@ -432,26 +357,34 @@ class HomeFragment :
.launch(PERMISSION_READ_AUDIO)
}
}
+ binding.homeIndexingMore.visibility = View.GONE
}
is NoMusicException -> {
logD("Showing no music error")
binding.homeIndexingStatus.text = context.getString(R.string.err_no_music)
// Configure the action to act as a reload trigger.
- binding.homeIndexingAction.apply {
+ binding.homeIndexingTry.apply {
visibility = View.VISIBLE
text = context.getString(R.string.lbl_retry)
setOnClickListener { musicModel.refresh() }
}
+ binding.homeIndexingMore.visibility = View.GONE
}
else -> {
logD("Showing generic error")
binding.homeIndexingStatus.text = context.getString(R.string.err_index_failed)
// Configure the action to act as a reload trigger.
- binding.homeIndexingAction.apply {
+ binding.homeIndexingTry.apply {
visibility = View.VISIBLE
text = context.getString(R.string.lbl_retry)
setOnClickListener { musicModel.rescan() }
}
+ binding.homeIndexingMore.apply {
+ visibility = View.VISIBLE
+ setOnClickListener {
+ findNavController().navigateSafe(HomeFragmentDirections.reportError(error))
+ }
+ }
}
}
}
@@ -460,7 +393,7 @@ class HomeFragment :
// Remove all content except for the progress indicator.
binding.homeIndexingContainer.visibility = View.VISIBLE
binding.homeIndexingProgress.visibility = View.VISIBLE
- binding.homeIndexingAction.visibility = View.INVISIBLE
+ binding.homeIndexingActions.visibility = View.INVISIBLE
when (progress) {
is IndexingProgress.Indeterminate -> {
@@ -483,33 +416,27 @@ class HomeFragment :
private fun handleDecision(decision: PlaylistDecision?) {
if (decision == null) return
- when (decision) {
- is PlaylistDecision.New -> {
- logD("Creating new playlist")
- findNavController()
- .navigateSafe(
- HomeFragmentDirections.newPlaylist(
- decision.songs.map { it.uid }.toTypedArray()))
+ val directions =
+ when (decision) {
+ is PlaylistDecision.New -> {
+ logD("Creating new playlist")
+ HomeFragmentDirections.newPlaylist(decision.songs.map { it.uid }.toTypedArray())
+ }
+ is PlaylistDecision.Rename -> {
+ logD("Renaming ${decision.playlist}")
+ HomeFragmentDirections.renamePlaylist(decision.playlist.uid)
+ }
+ is PlaylistDecision.Delete -> {
+ logD("Deleting ${decision.playlist}")
+ HomeFragmentDirections.deletePlaylist(decision.playlist.uid)
+ }
+ is PlaylistDecision.Add -> {
+ logD("Adding ${decision.songs.size} to a playlist")
+ HomeFragmentDirections.addToPlaylist(
+ decision.songs.map { it.uid }.toTypedArray())
+ }
}
- is PlaylistDecision.Rename -> {
- logD("Renaming ${decision.playlist}")
- findNavController()
- .navigateSafe(HomeFragmentDirections.renamePlaylist(decision.playlist.uid))
- }
- is PlaylistDecision.Delete -> {
- logD("Deleting ${decision.playlist}")
- findNavController()
- .navigateSafe(HomeFragmentDirections.deletePlaylist(decision.playlist.uid))
- }
- is PlaylistDecision.Add -> {
- logD("Adding ${decision.songs.size} to a playlist")
- findNavController()
- .navigateSafe(
- HomeFragmentDirections.addToPlaylist(
- decision.songs.map { it.uid }.toTypedArray()))
- }
- }
- musicModel.playlistDecision.consume()
+ findNavController().navigateSafe(directions)
}
private fun updateFab(songs: List, isFastScrolling: Boolean) {
@@ -532,44 +459,40 @@ class HomeFragment :
logD("Navigating to ${show.song}")
findNavController().navigateSafe(HomeFragmentDirections.showSong(show.song.uid))
}
-
- // Songs should be scrolled to if the album matches, or a new detail
- // fragment should be launched otherwise.
is Show.SongAlbumDetails -> {
logD("Navigating to the album of ${show.song}")
- setupAxisTransitions(MaterialSharedAxis.X)
+ applyAxisTransition(MaterialSharedAxis.X)
findNavController()
.navigateSafe(HomeFragmentDirections.showAlbum(show.song.album.uid))
}
-
- // If the album matches, no need to do anything. Otherwise launch a new
- // detail fragment.
is Show.AlbumDetails -> {
logD("Navigating to ${show.album}")
- setupAxisTransitions(MaterialSharedAxis.X)
+ applyAxisTransition(MaterialSharedAxis.X)
findNavController().navigateSafe(HomeFragmentDirections.showAlbum(show.album.uid))
}
-
- // Always launch a new ArtistDetailFragment.
is Show.ArtistDetails -> {
logD("Navigating to ${show.artist}")
- setupAxisTransitions(MaterialSharedAxis.X)
+ applyAxisTransition(MaterialSharedAxis.X)
findNavController().navigateSafe(HomeFragmentDirections.showArtist(show.artist.uid))
}
- is Show.SongArtistDetails -> {
+ is Show.SongArtistDecision -> {
logD("Navigating to artist choices for ${show.song}")
- findNavController().navigateSafe(HomeFragmentDirections.showArtists(show.song.uid))
+ findNavController()
+ .navigateSafe(HomeFragmentDirections.showArtistChoices(show.song.uid))
}
- is Show.AlbumArtistDetails -> {
+ is Show.AlbumArtistDecision -> {
logD("Navigating to artist choices for ${show.album}")
- findNavController().navigateSafe(HomeFragmentDirections.showArtists(show.album.uid))
+ findNavController()
+ .navigateSafe(HomeFragmentDirections.showArtistChoices(show.album.uid))
}
is Show.GenreDetails -> {
logD("Navigating to ${show.genre}")
+ applyAxisTransition(MaterialSharedAxis.X)
findNavController().navigateSafe(HomeFragmentDirections.showGenre(show.genre.uid))
}
is Show.PlaylistDetails -> {
logD("Navigating to ${show.playlist}")
+ applyAxisTransition(MaterialSharedAxis.X)
findNavController()
.navigateSafe(HomeFragmentDirections.showPlaylist(show.playlist.uid))
}
@@ -577,6 +500,20 @@ class HomeFragment :
}
}
+ private fun handleMenu(menu: Menu?) {
+ if (menu == null) return
+ val directions =
+ when (menu) {
+ is Menu.ForSong -> HomeFragmentDirections.openSongMenu(menu.parcel)
+ is Menu.ForAlbum -> HomeFragmentDirections.openAlbumMenu(menu.parcel)
+ is Menu.ForArtist -> HomeFragmentDirections.openArtistMenu(menu.parcel)
+ is Menu.ForGenre -> HomeFragmentDirections.openGenreMenu(menu.parcel)
+ is Menu.ForPlaylist -> HomeFragmentDirections.openPlaylistMenu(menu.parcel)
+ is Menu.ForSelection -> HomeFragmentDirections.openSelectionMenu(menu.parcel)
+ }
+ findNavController().navigateSafe(directions)
+ }
+
private fun updateSelection(selected: List) {
val binding = requireBinding()
if (selected.isNotEmpty()) {
@@ -591,7 +528,7 @@ class HomeFragment :
}
}
- private fun setupAxisTransitions(axis: Int) {
+ private fun applyAxisTransition(axis: Int) {
// Sanity check to avoid in-correct axis transitions
check(axis == MaterialSharedAxis.X || axis == MaterialSharedAxis.Z) {
"Not expecting Y axis transition"
@@ -612,25 +549,25 @@ class HomeFragment :
* [FragmentStateAdapter].
*/
private class HomePagerAdapter(
- private val tabs: List,
+ private val tabs: List,
fragmentManager: FragmentManager,
lifecycleOwner: LifecycleOwner
) : FragmentStateAdapter(fragmentManager, lifecycleOwner.lifecycle) {
override fun getItemCount() = tabs.size
+
override fun createFragment(position: Int): Fragment =
when (tabs[position]) {
- MusicMode.SONGS -> SongListFragment()
- MusicMode.ALBUMS -> AlbumListFragment()
- MusicMode.ARTISTS -> ArtistListFragment()
- MusicMode.GENRES -> GenreListFragment()
- MusicMode.PLAYLISTS -> PlaylistListFragment()
+ MusicType.SONGS -> SongListFragment()
+ MusicType.ALBUMS -> AlbumListFragment()
+ MusicType.ARTISTS -> ArtistListFragment()
+ MusicType.GENRES -> GenreListFragment()
+ MusicType.PLAYLISTS -> PlaylistListFragment()
}
}
private companion object {
val VP_RECYCLER_FIELD: Field by lazyReflectedField(ViewPager2::class, "mRecyclerView")
val RV_TOUCH_SLOP_FIELD: Field by lazyReflectedField(RecyclerView::class, "mTouchSlop")
- const val KEY_LAST_TRANSITION_AXIS =
- BuildConfig.APPLICATION_ID + ".key.LAST_TRANSITION_AXIS"
+ const val KEY_LAST_TRANSITION_ID = BuildConfig.APPLICATION_ID + ".key.LAST_TRANSITION_AXIS"
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt
index 4e468ec95..5fc218cfe 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt
@@ -24,7 +24,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.Tab
-import org.oxycblt.auxio.music.MusicMode
+import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
@@ -75,9 +75,9 @@ class HomeSettingsImpl @Inject constructor(@ApplicationContext context: Context)
logD("Old tabs: $oldTabs")
// The playlist tab is now parsed, but it needs to be made visible.
- val playlistIndex = oldTabs.indexOfFirst { it.mode == MusicMode.PLAYLISTS }
+ val playlistIndex = oldTabs.indexOfFirst { it.type == MusicType.PLAYLISTS }
check(playlistIndex > -1) // This should exist, otherwise we are in big trouble
- oldTabs[playlistIndex] = Tab.Visible(MusicMode.PLAYLISTS)
+ oldTabs[playlistIndex] = Tab.Visible(MusicType.PLAYLISTS)
logD("New tabs: $oldTabs")
sharedPreferences.edit {
diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt
index 4e471758a..bb9311c84 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt
@@ -24,16 +24,17 @@ import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.home.tabs.Tab
-import org.oxycblt.auxio.list.Sort
+import org.oxycblt.auxio.list.ListSettings
import org.oxycblt.auxio.list.adapter.UpdateInstructions
+import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
-import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicRepository
-import org.oxycblt.auxio.music.MusicSettings
+import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
+import org.oxycblt.auxio.playback.PlaySong
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.util.Event
import org.oxycblt.auxio.util.MutableEvent
@@ -49,73 +50,98 @@ class HomeViewModel
@Inject
constructor(
private val homeSettings: HomeSettings,
+ private val listSettings: ListSettings,
private val playbackSettings: PlaybackSettings,
private val musicRepository: MusicRepository,
- private val musicSettings: MusicSettings
) : ViewModel(), MusicRepository.UpdateListener, HomeSettings.Listener {
- private val _songsList = MutableStateFlow(listOf())
+ private val _songList = MutableStateFlow(listOf())
/** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */
- val songsList: StateFlow
>
- get() = _songsList
- private val _songsInstructions = MutableEvent()
- /** Instructions for how to update [songsList] in the UI. */
- val songsInstructions: Event
- get() = _songsInstructions
+ val songList: StateFlow>
+ get() = _songList
- private val _albumsLists = MutableStateFlow(listOf())
+ private val _songInstructions = MutableEvent()
+ /** Instructions for how to update [songList] in the UI. */
+ val songInstructions: Event
+ get() = _songInstructions
+
+ /** The current [Sort] used for [songList]. */
+ val songSort: Sort
+ get() = listSettings.songSort
+
+ /** The [PlaySong] instructions to use when playing a [Song]. */
+ val playWith
+ get() = playbackSettings.playInListWith
+
+ private val _albumList = MutableStateFlow(listOf())
/** A list of [Album]s, sorted by the preferred [Sort], to be shown in the home view. */
- val albumsList: StateFlow>
- get() = _albumsLists
- private val _albumsInstructions = MutableEvent()
- /** Instructions for how to update [albumsList] in the UI. */
- val albumsInstructions: Event
- get() = _albumsInstructions
+ val albumList: StateFlow>
+ get() = _albumList
- private val _artistsList = MutableStateFlow(listOf())
+ private val _albumInstructions = MutableEvent()
+ /** Instructions for how to update [albumList] in the UI. */
+ val albumInstructions: Event
+ get() = _albumInstructions
+
+ /** The current [Sort] used for [albumList]. */
+ val albumSort: Sort
+ get() = listSettings.albumSort
+
+ private val _artistList = MutableStateFlow(listOf())
/**
* A list of [Artist]s, sorted by the preferred [Sort], to be shown in the home view. Note that
* if "Hide collaborators" is on, this list will not include collaborator [Artist]s.
*/
- val artistsList: MutableStateFlow>
- get() = _artistsList
- private val _artistsInstructions = MutableEvent()
- /** Instructions for how to update [artistsList] in the UI. */
- val artistsInstructions: Event
- get() = _artistsInstructions
+ val artistList: MutableStateFlow>
+ get() = _artistList
- private val _genresList = MutableStateFlow(listOf())
+ private val _artistInstructions = MutableEvent()
+ /** Instructions for how to update [artistList] in the UI. */
+ val artistInstructions: Event
+ get() = _artistInstructions
+
+ /** The current [Sort] used for [artistList]. */
+ val artistSort: Sort
+ get() = listSettings.artistSort
+
+ private val _genreList = MutableStateFlow(listOf())
/** A list of [Genre]s, sorted by the preferred [Sort], to be shown in the home view. */
- val genresList: StateFlow>
- get() = _genresList
- private val _genresInstructions = MutableEvent()
- /** Instructions for how to update [genresList] in the UI. */
- val genresInstructions: Event
- get() = _genresInstructions
+ val genreList: StateFlow>
+ get() = _genreList
- private val _playlistsList = MutableStateFlow(listOf())
+ private val _genreInstructions = MutableEvent()
+ /** Instructions for how to update [genreList] in the UI. */
+ val genreInstructions: Event
+ get() = _genreInstructions
+
+ /** The current [Sort] used for [genreList]. */
+ val genreSort: Sort
+ get() = listSettings.genreSort
+
+ private val _playlistList = MutableStateFlow(listOf())
/** A list of [Playlist]s, sorted by the preferred [Sort], to be shown in the home view. */
- val playlistsList: StateFlow>
- get() = _playlistsList
- private val _playlistsInstructions = MutableEvent()
- /** Instructions for how to update [genresList] in the UI. */
- val playlistsInstructions: Event
- get() = _playlistsInstructions
+ val playlistList: StateFlow>
+ get() = _playlistList
- /** The [MusicMode] to use when playing a [Song] from the UI. */
- val playbackMode: MusicMode
- get() = playbackSettings.inListPlaybackMode
+ private val _playlistInstructions = MutableEvent()
+ /** Instructions for how to update [genreList] in the UI. */
+ val playlistInstructions: Event
+ get() = _playlistInstructions
+
+ /** The current [Sort] used for [genreList]. */
+ val playlistSort: Sort
+ get() = listSettings.playlistSort
/**
- * A list of [MusicMode] corresponding to the current [Tab] configuration, excluding invisible
+ * A list of [MusicType] corresponding to the current [Tab] configuration, excluding invisible
* [Tab]s.
*/
- var currentTabModes = makeTabModes()
+ var currentTabTypes = makeTabTypes()
private set
- private val _currentTabMode = MutableStateFlow(currentTabModes[0])
- /** The [MusicMode] of the currently shown [Tab]. */
- val currentTabMode: StateFlow = _currentTabMode
+ private val _currentTabType = MutableStateFlow(currentTabTypes[0])
+ /** The [MusicType] of the currently shown [Tab]. */
+ val currentTabType: StateFlow = _currentTabType
private val _shouldRecreate = MutableEvent()
/**
@@ -130,6 +156,10 @@ constructor(
/** A marker for whether the user is fast-scrolling in the home view or not. */
val isFastScrolling: StateFlow = _isFastScrolling
+ private val _showOuter = MutableEvent()
+ val showOuter: Event
+ get() = _showOuter
+
init {
musicRepository.addUpdateListener(this)
homeSettings.registerListener(this)
@@ -147,13 +177,13 @@ constructor(
logD("Refreshing library")
// Get the each list of items in the library to use as our list data.
// Applying the preferred sorting to them.
- _songsInstructions.put(UpdateInstructions.Diff)
- _songsList.value = musicSettings.songSort.songs(deviceLibrary.songs)
- _albumsInstructions.put(UpdateInstructions.Diff)
- _albumsLists.value = musicSettings.albumSort.albums(deviceLibrary.albums)
- _artistsInstructions.put(UpdateInstructions.Diff)
- _artistsList.value =
- musicSettings.artistSort.artists(
+ _songInstructions.put(UpdateInstructions.Diff)
+ _songList.value = listSettings.songSort.songs(deviceLibrary.songs)
+ _albumInstructions.put(UpdateInstructions.Diff)
+ _albumList.value = listSettings.albumSort.albums(deviceLibrary.albums)
+ _artistInstructions.put(UpdateInstructions.Diff)
+ _artistList.value =
+ listSettings.artistSort.artists(
if (homeSettings.shouldHideCollaborators) {
logD("Filtering collaborator artists")
// Hide Collaborators is enabled, filter out collaborators.
@@ -162,22 +192,22 @@ constructor(
logD("Using all artists")
deviceLibrary.artists
})
- _genresInstructions.put(UpdateInstructions.Diff)
- _genresList.value = musicSettings.genreSort.genres(deviceLibrary.genres)
+ _genreInstructions.put(UpdateInstructions.Diff)
+ _genreList.value = listSettings.genreSort.genres(deviceLibrary.genres)
}
val userLibrary = musicRepository.userLibrary
if (changes.userLibrary && userLibrary != null) {
logD("Refreshing playlists")
- _playlistsInstructions.put(UpdateInstructions.Diff)
- _playlistsList.value = musicSettings.playlistSort.playlists(userLibrary.playlists)
+ _playlistInstructions.put(UpdateInstructions.Diff)
+ _playlistList.value = listSettings.playlistSort.playlists(userLibrary.playlists)
}
}
override fun onTabsChanged() {
// Tabs changed, update the current tabs and set up a re-create event.
- currentTabModes = makeTabModes()
- logD("Updating tabs: ${currentTabMode.value}")
+ currentTabTypes = makeTabTypes()
+ logD("Updating tabs: ${currentTabType.value}")
_shouldRecreate.put(Unit)
}
@@ -189,69 +219,68 @@ constructor(
}
/**
- * Get the preferred [Sort] for a given [Tab].
+ * Apply a new [Sort] to [songList].
*
- * @param tabMode The [MusicMode] of the [Tab] desired.
- * @return The [Sort] preferred for that [Tab]
+ * @param sort The [Sort] to apply.
*/
- fun getSortForTab(tabMode: MusicMode) =
- when (tabMode) {
- MusicMode.SONGS -> musicSettings.songSort
- MusicMode.ALBUMS -> musicSettings.albumSort
- MusicMode.ARTISTS -> musicSettings.artistSort
- MusicMode.GENRES -> musicSettings.genreSort
- MusicMode.PLAYLISTS -> musicSettings.playlistSort
- }
-
- /**
- * Update the preferred [Sort] for the current [Tab]. Will update corresponding list.
- *
- * @param sort The new [Sort] to apply. Assumed to be an allowed sort for the current [Tab].
- */
- fun setSortForCurrentTab(sort: Sort) {
- // Can simply re-sort the current list of items without having to access the library.
- when (val mode = _currentTabMode.value) {
- MusicMode.SONGS -> {
- logD("Updating song [$mode] sort mode to $sort")
- musicSettings.songSort = sort
- _songsInstructions.put(UpdateInstructions.Replace(0))
- _songsList.value = sort.songs(_songsList.value)
- }
- MusicMode.ALBUMS -> {
- logD("Updating album [$mode] sort mode to $sort")
- musicSettings.albumSort = sort
- _albumsInstructions.put(UpdateInstructions.Replace(0))
- _albumsLists.value = sort.albums(_albumsLists.value)
- }
- MusicMode.ARTISTS -> {
- logD("Updating artist [$mode] sort mode to $sort")
- musicSettings.artistSort = sort
- _artistsInstructions.put(UpdateInstructions.Replace(0))
- _artistsList.value = sort.artists(_artistsList.value)
- }
- MusicMode.GENRES -> {
- logD("Updating genre [$mode] sort mode to $sort")
- musicSettings.genreSort = sort
- _genresInstructions.put(UpdateInstructions.Replace(0))
- _genresList.value = sort.genres(_genresList.value)
- }
- MusicMode.PLAYLISTS -> {
- logD("Updating playlist [$mode] sort mode to $sort")
- musicSettings.playlistSort = sort
- _playlistsInstructions.put(UpdateInstructions.Replace(0))
- _playlistsList.value = sort.playlists(_playlistsList.value)
- }
- }
+ fun applySongSort(sort: Sort) {
+ listSettings.songSort = sort
+ _songInstructions.put(UpdateInstructions.Replace(0))
+ _songList.value = listSettings.songSort.songs(_songList.value)
}
/**
- * Update [currentTabMode] to reflect a new ViewPager2 position
+ * Apply a new [Sort] to [albumList].
+ *
+ * @param sort The [Sort] to apply.
+ */
+ fun applyAlbumSort(sort: Sort) {
+ listSettings.albumSort = sort
+ _albumInstructions.put(UpdateInstructions.Replace(0))
+ _albumList.value = listSettings.albumSort.albums(_albumList.value)
+ }
+
+ /**
+ * Apply a new [Sort] to [artistList].
+ *
+ * @param sort The [Sort] to apply.
+ */
+ fun applyArtistSort(sort: Sort) {
+ listSettings.artistSort = sort
+ _artistInstructions.put(UpdateInstructions.Replace(0))
+ _artistList.value = listSettings.artistSort.artists(_artistList.value)
+ }
+
+ /**
+ * Apply a new [Sort] to [genreList].
+ *
+ * @param sort The [Sort] to apply.
+ */
+ fun applyGenreSort(sort: Sort) {
+ listSettings.genreSort = sort
+ _genreInstructions.put(UpdateInstructions.Replace(0))
+ _genreList.value = listSettings.genreSort.genres(_genreList.value)
+ }
+
+ /**
+ * Apply a new [Sort] to [playlistList].
+ *
+ * @param sort The [Sort] to apply.
+ */
+ fun applyPlaylistSort(sort: Sort) {
+ listSettings.playlistSort = sort
+ _playlistInstructions.put(UpdateInstructions.Replace(0))
+ _playlistList.value = listSettings.playlistSort.playlists(_playlistList.value)
+ }
+
+ /**
+ * Update [currentTabType] to reflect a new ViewPager2 position
*
* @param pagerPos The new position of the ViewPager2 instance.
*/
fun synchronizeTabPosition(pagerPos: Int) {
- logD("Updating current tab to ${currentTabModes[pagerPos]}")
- _currentTabMode.value = currentTabModes[pagerPos]
+ logD("Updating current tab to ${currentTabTypes[pagerPos]}")
+ _currentTabType.value = currentTabTypes[pagerPos]
}
/**
@@ -264,12 +293,26 @@ constructor(
_isFastScrolling.value = isFastScrolling
}
+ fun showSettings() {
+ _showOuter.put(Outer.Settings)
+ }
+
+ fun showAbout() {
+ _showOuter.put(Outer.About)
+ }
+
/**
- * Create a list of [MusicMode]s representing a simpler version of the [Tab] configuration.
+ * Create a list of [MusicType]s representing a simpler version of the [Tab] configuration.
*
- * @return A list of the [MusicMode]s for each visible [Tab] in the configuration, ordered in
+ * @return A list of the [MusicType]s for each visible [Tab] in the configuration, ordered in
* the same way as the configuration.
*/
- private fun makeTabModes() =
- homeSettings.homeTabs.filterIsInstance().map { it.mode }
+ private fun makeTabTypes() =
+ homeSettings.homeTabs.filterIsInstance().map { it.type }
+}
+
+sealed interface Outer {
+ data object Settings : Outer
+
+ data object About : Outer
}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt
index 620ac018f..ae546d137 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt
@@ -123,8 +123,11 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0)
}
override fun isAutoMirrored(): Boolean = true
+
override fun setAlpha(alpha: Int) {}
+
override fun setColorFilter(colorFilter: ColorFilter?) {}
+
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
private fun updatePath() {
diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt
index b4aac9121..74c942dae 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt
@@ -21,7 +21,6 @@ package org.oxycblt.auxio.home.list
import android.os.Bundle
import android.text.format.DateUtils
import android.view.LayoutInflater
-import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import dagger.hilt.android.AndroidEntryPoint
@@ -32,14 +31,13 @@ import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.ListFragment
+import org.oxycblt.auxio.list.ListViewModel
import org.oxycblt.auxio.list.SelectableListListener
-import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.AlbumViewHolder
-import org.oxycblt.auxio.list.selection.SelectionViewModel
+import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Music
-import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Song
@@ -59,10 +57,10 @@ class AlbumListFragment :
FastScrollRecyclerView.Listener,
FastScrollRecyclerView.PopupProvider {
private val homeModel: HomeViewModel by activityViewModels()
- override val detailModel: DetailViewModel by activityViewModels()
- override val playbackModel: PlaybackViewModel by activityViewModels()
+ private val detailModel: DetailViewModel by activityViewModels()
+ override val listModel: ListViewModel by activityViewModels()
override val musicModel: MusicViewModel by activityViewModels()
- override val selectionModel: SelectionViewModel by activityViewModels()
+ override val playbackModel: PlaybackViewModel by activityViewModels()
private val albumAdapter = AlbumAdapter(this)
// Save memory by re-using the same formatter and string builder when creating popup text
private val formatterSb = StringBuilder(64)
@@ -81,8 +79,8 @@ class AlbumListFragment :
listener = this@AlbumListFragment
}
- collectImmediately(homeModel.albumsList, ::updateAlbums)
- collectImmediately(selectionModel.selected, ::updateSelection)
+ collectImmediately(homeModel.albumList, ::updateAlbums)
+ collectImmediately(listModel.selected, ::updateSelection)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
@@ -97,9 +95,9 @@ class AlbumListFragment :
}
override fun getPopup(pos: Int): String? {
- val album = homeModel.albumsList.value[pos]
+ val album = homeModel.albumList.value[pos]
// Change how we display the popup depending on the current sort mode.
- return when (homeModel.getSortForTab(MusicMode.ALBUMS).mode) {
+ return when (homeModel.albumSort.mode) {
// By Name -> Use Name
is Sort.Mode.ByName -> album.name.thumb
@@ -141,12 +139,12 @@ class AlbumListFragment :
detailModel.showAlbum(item)
}
- override fun onOpenMenu(item: Album, anchor: View) {
- openMusicMenu(anchor, R.menu.menu_album_actions, item)
+ override fun onOpenMenu(item: Album) {
+ listModel.openMenu(R.menu.album, item)
}
private fun updateAlbums(albums: List) {
- albumAdapter.update(albums, homeModel.albumsInstructions.consume())
+ albumAdapter.update(albums, homeModel.albumInstructions.consume())
}
private fun updateSelection(selection: List) {
diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt
index b66c6e965..7dc885308 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt
@@ -20,7 +20,6 @@ package org.oxycblt.auxio.home.list
import android.os.Bundle
import android.view.LayoutInflater
-import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import dagger.hilt.android.AndroidEntryPoint
@@ -30,21 +29,20 @@ import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.ListFragment
+import org.oxycblt.auxio.list.ListViewModel
import org.oxycblt.auxio.list.SelectableListListener
-import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.ArtistViewHolder
-import org.oxycblt.auxio.list.selection.SelectionViewModel
+import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
-import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.collectImmediately
-import org.oxycblt.auxio.util.nonZeroOrNull
+import org.oxycblt.auxio.util.positiveOrNull
/**
* A [ListFragment] that shows a list of [Artist]s.
@@ -57,10 +55,10 @@ class ArtistListFragment :
FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.Listener {
private val homeModel: HomeViewModel by activityViewModels()
- override val detailModel: DetailViewModel by activityViewModels()
- override val playbackModel: PlaybackViewModel by activityViewModels()
+ private val detailModel: DetailViewModel by activityViewModels()
+ override val listModel: ListViewModel by activityViewModels()
override val musicModel: MusicViewModel by activityViewModels()
- override val selectionModel: SelectionViewModel by activityViewModels()
+ override val playbackModel: PlaybackViewModel by activityViewModels()
private val artistAdapter = ArtistAdapter(this)
override fun onCreateBinding(inflater: LayoutInflater) =
@@ -76,8 +74,8 @@ class ArtistListFragment :
listener = this@ArtistListFragment
}
- collectImmediately(homeModel.artistsList, ::updateArtists)
- collectImmediately(selectionModel.selected, ::updateSelection)
+ collectImmediately(homeModel.artistList, ::updateArtists)
+ collectImmediately(listModel.selected, ::updateSelection)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
@@ -92,9 +90,9 @@ class ArtistListFragment :
}
override fun getPopup(pos: Int): String? {
- val artist = homeModel.artistsList.value[pos]
+ val artist = homeModel.artistList.value[pos]
// Change how we display the popup depending on the current sort mode.
- return when (homeModel.getSortForTab(MusicMode.ARTISTS).mode) {
+ return when (homeModel.artistSort.mode) {
// By Name -> Use Name
is Sort.Mode.ByName -> artist.name.thumb
@@ -102,7 +100,7 @@ class ArtistListFragment :
is Sort.Mode.ByDuration -> artist.durationMs?.formatDurationMs(false)
// Count -> Use song count
- is Sort.Mode.ByCount -> artist.songs.size.nonZeroOrNull()?.toString()
+ is Sort.Mode.ByCount -> artist.songs.size.positiveOrNull()?.toString()
// Unsupported sort, error gracefully
else -> null
@@ -117,12 +115,12 @@ class ArtistListFragment :
detailModel.showArtist(item)
}
- override fun onOpenMenu(item: Artist, anchor: View) {
- openMusicMenu(anchor, R.menu.menu_parent_actions, item)
+ override fun onOpenMenu(item: Artist) {
+ listModel.openMenu(R.menu.parent, item)
}
private fun updateArtists(artists: List) {
- artistAdapter.update(artists, homeModel.artistsInstructions.consume())
+ artistAdapter.update(artists, homeModel.artistInstructions.consume())
}
private fun updateSelection(selection: List) {
diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt
index d751e3699..3307fa721 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt
@@ -20,7 +20,6 @@ package org.oxycblt.auxio.home.list
import android.os.Bundle
import android.view.LayoutInflater
-import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import dagger.hilt.android.AndroidEntryPoint
@@ -30,14 +29,13 @@ import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.ListFragment
+import org.oxycblt.auxio.list.ListViewModel
import org.oxycblt.auxio.list.SelectableListListener
-import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.GenreViewHolder
-import org.oxycblt.auxio.list.selection.SelectionViewModel
+import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
-import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Song
@@ -56,10 +54,10 @@ class GenreListFragment :
FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.Listener {
private val homeModel: HomeViewModel by activityViewModels()
- override val detailModel: DetailViewModel by activityViewModels()
- override val playbackModel: PlaybackViewModel by activityViewModels()
+ private val detailModel: DetailViewModel by activityViewModels()
+ override val listModel: ListViewModel by activityViewModels()
override val musicModel: MusicViewModel by activityViewModels()
- override val selectionModel: SelectionViewModel by activityViewModels()
+ override val playbackModel: PlaybackViewModel by activityViewModels()
private val genreAdapter = GenreAdapter(this)
override fun onCreateBinding(inflater: LayoutInflater) =
@@ -75,8 +73,8 @@ class GenreListFragment :
listener = this@GenreListFragment
}
- collectImmediately(homeModel.genresList, ::updateGenres)
- collectImmediately(selectionModel.selected, ::updateSelection)
+ collectImmediately(homeModel.genreList, ::updateGenres)
+ collectImmediately(listModel.selected, ::updateSelection)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
@@ -91,9 +89,9 @@ class GenreListFragment :
}
override fun getPopup(pos: Int): String? {
- val genre = homeModel.genresList.value[pos]
+ val genre = homeModel.genreList.value[pos]
// Change how we display the popup depending on the current sort mode.
- return when (homeModel.getSortForTab(MusicMode.GENRES).mode) {
+ return when (homeModel.genreSort.mode) {
// By Name -> Use Name
is Sort.Mode.ByName -> genre.name.thumb
@@ -116,12 +114,12 @@ class GenreListFragment :
detailModel.showGenre(item)
}
- override fun onOpenMenu(item: Genre, anchor: View) {
- openMusicMenu(anchor, R.menu.menu_parent_actions, item)
+ override fun onOpenMenu(item: Genre) {
+ listModel.openMenu(R.menu.parent, item)
}
private fun updateGenres(genres: List) {
- genreAdapter.update(genres, homeModel.genresInstructions.consume())
+ genreAdapter.update(genres, homeModel.genreInstructions.consume())
}
private fun updateSelection(selection: List) {
diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt
index 405dbe312..4228c872a 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt
@@ -20,7 +20,6 @@ package org.oxycblt.auxio.home.list
import android.os.Bundle
import android.view.LayoutInflater
-import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import org.oxycblt.auxio.R
@@ -29,13 +28,12 @@ import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.ListFragment
+import org.oxycblt.auxio.list.ListViewModel
import org.oxycblt.auxio.list.SelectableListListener
-import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.PlaylistViewHolder
-import org.oxycblt.auxio.list.selection.SelectionViewModel
+import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Music
-import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Playlist
@@ -54,10 +52,10 @@ class PlaylistListFragment :
FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.Listener {
private val homeModel: HomeViewModel by activityViewModels()
- override val detailModel: DetailViewModel by activityViewModels()
- override val playbackModel: PlaybackViewModel by activityViewModels()
+ private val detailModel: DetailViewModel by activityViewModels()
+ override val listModel: ListViewModel by activityViewModels()
override val musicModel: MusicViewModel by activityViewModels()
- override val selectionModel: SelectionViewModel by activityViewModels()
+ override val playbackModel: PlaybackViewModel by activityViewModels()
private val playlistAdapter = PlaylistAdapter(this)
override fun onCreateBinding(inflater: LayoutInflater) =
@@ -73,8 +71,8 @@ class PlaylistListFragment :
listener = this@PlaylistListFragment
}
- collectImmediately(homeModel.playlistsList, ::updatePlaylists)
- collectImmediately(selectionModel.selected, ::updateSelection)
+ collectImmediately(homeModel.playlistList, ::updatePlaylists)
+ collectImmediately(listModel.selected, ::updateSelection)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
@@ -89,9 +87,9 @@ class PlaylistListFragment :
}
override fun getPopup(pos: Int): String? {
- val playlist = homeModel.playlistsList.value[pos]
+ val playlist = homeModel.playlistList.value[pos]
// Change how we display the popup depending on the current sort mode.
- return when (homeModel.getSortForTab(MusicMode.GENRES).mode) {
+ return when (homeModel.playlistSort.mode) {
// By Name -> Use Name
is Sort.Mode.ByName -> playlist.name.thumb
@@ -114,12 +112,12 @@ class PlaylistListFragment :
detailModel.showPlaylist(item)
}
- override fun onOpenMenu(item: Playlist, anchor: View) {
- openMusicMenu(anchor, R.menu.menu_playlist_actions, item)
+ override fun onOpenMenu(item: Playlist) {
+ listModel.openMenu(R.menu.playlist, item)
}
private fun updatePlaylists(playlists: List) {
- playlistAdapter.update(playlists, homeModel.playlistsInstructions.consume())
+ playlistAdapter.update(playlists, homeModel.playlistInstructions.consume())
}
private fun updateSelection(selection: List) {
diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt
index d827adbf7..04f9847f1 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt
@@ -21,24 +21,21 @@ package org.oxycblt.auxio.home.list
import android.os.Bundle
import android.text.format.DateUtils
import android.view.LayoutInflater
-import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import dagger.hilt.android.AndroidEntryPoint
import java.util.Formatter
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
-import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.ListFragment
+import org.oxycblt.auxio.list.ListViewModel
import org.oxycblt.auxio.list.SelectableListListener
-import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SongViewHolder
-import org.oxycblt.auxio.list.selection.SelectionViewModel
+import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Music
-import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Song
@@ -58,10 +55,9 @@ class SongListFragment :
FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.Listener {
private val homeModel: HomeViewModel by activityViewModels()
- override val detailModel: DetailViewModel by activityViewModels()
- override val playbackModel: PlaybackViewModel by activityViewModels()
+ override val listModel: ListViewModel by activityViewModels()
override val musicModel: MusicViewModel by activityViewModels()
- override val selectionModel: SelectionViewModel by activityViewModels()
+ override val playbackModel: PlaybackViewModel by activityViewModels()
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)
@@ -80,8 +76,8 @@ class SongListFragment :
listener = this@SongListFragment
}
- collectImmediately(homeModel.songsList, ::updateSongs)
- collectImmediately(selectionModel.selected, ::updateSelection)
+ collectImmediately(homeModel.songList, ::updateSongs)
+ collectImmediately(listModel.selected, ::updateSelection)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
@@ -96,11 +92,11 @@ class SongListFragment :
}
override fun getPopup(pos: Int): String? {
- val song = homeModel.songsList.value[pos]
+ val song = homeModel.songList.value[pos]
// Change how we display the popup depending on the current sort mode.
// Note: We don't use the more correct individual artist name here, as sorts are largely
// based off the names of the parent objects and not the child objects.
- return when (homeModel.getSortForTab(MusicMode.SONGS).mode) {
+ return when (homeModel.songSort.mode) {
// Name -> Use name
is Sort.Mode.ByName -> song.name.thumb
@@ -139,15 +135,15 @@ class SongListFragment :
}
override fun onRealClick(item: Song) {
- playbackModel.playFrom(item, homeModel.playbackMode)
+ playbackModel.play(item, homeModel.playWith)
}
- override fun onOpenMenu(item: Song, anchor: View) {
- openMusicMenu(anchor, R.menu.menu_song_actions, item)
+ override fun onOpenMenu(item: Song) {
+ listModel.openMenu(R.menu.song, item, homeModel.playWith)
}
private fun updateSongs(songs: List) {
- songAdapter.update(songs, homeModel.songsInstructions.consume())
+ songAdapter.update(songs, homeModel.songInstructions.consume())
}
private fun updateSelection(selection: List) {
diff --git a/app/src/main/java/org/oxycblt/auxio/home/sort/AlbumSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/sort/AlbumSortDialog.kt
new file mode 100644
index 000000000..39efb1ca1
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/home/sort/AlbumSortDialog.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ * AlbumSortDialog.kt is part of Auxio.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.auxio.home.sort
+
+import androidx.fragment.app.activityViewModels
+import dagger.hilt.android.AndroidEntryPoint
+import org.oxycblt.auxio.home.HomeViewModel
+import org.oxycblt.auxio.list.sort.Sort
+import org.oxycblt.auxio.list.sort.SortDialog
+
+/**
+ * A [SortDialog] that controls the [Sort] of [HomeViewModel.albumList].
+ *
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+@AndroidEntryPoint
+class AlbumSortDialog : SortDialog() {
+ private val homeModel: HomeViewModel by activityViewModels()
+
+ override fun getInitialSort() = homeModel.albumSort
+
+ override fun applyChosenSort(sort: Sort) {
+ homeModel.applyAlbumSort(sort)
+ }
+
+ override fun getModeChoices() =
+ listOf(
+ Sort.Mode.ByName,
+ Sort.Mode.ByArtist,
+ Sort.Mode.ByDate,
+ Sort.Mode.ByDuration,
+ Sort.Mode.ByCount,
+ Sort.Mode.ByDateAdded)
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/sort/ArtistSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/sort/ArtistSortDialog.kt
new file mode 100644
index 000000000..f3aeacd17
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/home/sort/ArtistSortDialog.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ * ArtistSortDialog.kt is part of Auxio.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.auxio.home.sort
+
+import androidx.fragment.app.activityViewModels
+import dagger.hilt.android.AndroidEntryPoint
+import org.oxycblt.auxio.home.HomeViewModel
+import org.oxycblt.auxio.list.sort.Sort
+import org.oxycblt.auxio.list.sort.SortDialog
+
+/**
+ * A [SortDialog] that controls the [Sort] of [HomeViewModel.artistList].
+ *
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+@AndroidEntryPoint
+class ArtistSortDialog : SortDialog() {
+ private val homeModel: HomeViewModel by activityViewModels()
+
+ override fun getInitialSort() = homeModel.artistSort
+
+ override fun applyChosenSort(sort: Sort) {
+ homeModel.applyArtistSort(sort)
+ }
+
+ override fun getModeChoices() =
+ listOf(Sort.Mode.ByName, Sort.Mode.ByDuration, Sort.Mode.ByCount)
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/sort/GenreSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/sort/GenreSortDialog.kt
new file mode 100644
index 000000000..e62ac8272
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/home/sort/GenreSortDialog.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ * GenreSortDialog.kt is part of Auxio.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.auxio.home.sort
+
+import androidx.fragment.app.activityViewModels
+import dagger.hilt.android.AndroidEntryPoint
+import org.oxycblt.auxio.home.HomeViewModel
+import org.oxycblt.auxio.list.sort.Sort
+import org.oxycblt.auxio.list.sort.SortDialog
+
+/**
+ * A [SortDialog] that controls the [Sort] of [HomeViewModel.genreList].
+ *
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+@AndroidEntryPoint
+class GenreSortDialog : SortDialog() {
+ private val homeModel: HomeViewModel by activityViewModels()
+
+ override fun getInitialSort() = homeModel.genreSort
+
+ override fun applyChosenSort(sort: Sort) {
+ homeModel.applyGenreSort(sort)
+ }
+
+ override fun getModeChoices() =
+ listOf(Sort.Mode.ByName, Sort.Mode.ByDuration, Sort.Mode.ByCount)
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/sort/PlaylistSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/sort/PlaylistSortDialog.kt
new file mode 100644
index 000000000..d87f126e9
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/home/sort/PlaylistSortDialog.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ * PlaylistSortDialog.kt is part of Auxio.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.auxio.home.sort
+
+import androidx.fragment.app.activityViewModels
+import dagger.hilt.android.AndroidEntryPoint
+import org.oxycblt.auxio.home.HomeViewModel
+import org.oxycblt.auxio.list.sort.Sort
+import org.oxycblt.auxio.list.sort.SortDialog
+
+/**
+ * A [SortDialog] that controls the [Sort] of [HomeViewModel.playlistList].
+ *
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+@AndroidEntryPoint
+class PlaylistSortDialog : SortDialog() {
+ private val homeModel: HomeViewModel by activityViewModels()
+
+ override fun getInitialSort() = homeModel.playlistSort
+
+ override fun applyChosenSort(sort: Sort) {
+ homeModel.applyPlaylistSort(sort)
+ }
+
+ override fun getModeChoices() =
+ listOf(Sort.Mode.ByName, Sort.Mode.ByDuration, Sort.Mode.ByCount)
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/sort/SongSortDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/sort/SongSortDialog.kt
new file mode 100644
index 000000000..a961a39c4
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/home/sort/SongSortDialog.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ * SongSortDialog.kt is part of Auxio.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.auxio.home.sort
+
+import androidx.fragment.app.activityViewModels
+import dagger.hilt.android.AndroidEntryPoint
+import org.oxycblt.auxio.home.HomeViewModel
+import org.oxycblt.auxio.list.sort.Sort
+import org.oxycblt.auxio.list.sort.SortDialog
+
+/**
+ * A [SortDialog] that controls the [Sort] of [HomeViewModel.songList].
+ *
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+@AndroidEntryPoint
+class SongSortDialog : SortDialog() {
+ private val homeModel: HomeViewModel by activityViewModels()
+
+ override fun getInitialSort() = homeModel.songSort
+
+ override fun applyChosenSort(sort: Sort) {
+ homeModel.applySongSort(sort)
+ }
+
+ override fun getModeChoices() =
+ listOf(
+ Sort.Mode.ByName,
+ Sort.Mode.ByArtist,
+ Sort.Mode.ByAlbum,
+ Sort.Mode.ByDate,
+ Sort.Mode.ByDuration,
+ Sort.Mode.ByDateAdded)
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt
index 36aed93bf..73170ef4c 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt
@@ -22,7 +22,7 @@ import android.content.Context
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import org.oxycblt.auxio.R
-import org.oxycblt.auxio.music.MusicMode
+import org.oxycblt.auxio.music.MusicType
/**
* A [TabLayoutMediator.TabConfigurationStrategy] that uses larger/smaller tab configurations
@@ -32,7 +32,7 @@ import org.oxycblt.auxio.music.MusicMode
* @param tabs Current tab configuration from settings
* @author Alexander Capehart (OxygenCobalt)
*/
-class AdaptiveTabStrategy(context: Context, private val tabs: List) :
+class AdaptiveTabStrategy(context: Context, private val tabs: List) :
TabLayoutMediator.TabConfigurationStrategy {
private val width = context.resources.configuration.smallestScreenWidthDp
@@ -41,23 +41,23 @@ class AdaptiveTabStrategy(context: Context, private val tabs: List) :
val string: Int
when (tabs[position]) {
- MusicMode.SONGS -> {
+ MusicType.SONGS -> {
icon = R.drawable.ic_song_24
string = R.string.lbl_songs
}
- MusicMode.ALBUMS -> {
+ MusicType.ALBUMS -> {
icon = R.drawable.ic_album_24
string = R.string.lbl_albums
}
- MusicMode.ARTISTS -> {
+ MusicType.ARTISTS -> {
icon = R.drawable.ic_artist_24
string = R.string.lbl_artists
}
- MusicMode.GENRES -> {
+ MusicType.GENRES -> {
icon = R.drawable.ic_genre_24
string = R.string.lbl_genres
}
- MusicMode.PLAYLISTS -> {
+ MusicType.PLAYLISTS -> {
icon = R.drawable.ic_playlist_24
string = R.string.lbl_playlists
}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt
index 5cacd084b..aee964e45 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt
@@ -18,30 +18,30 @@
package org.oxycblt.auxio.home.tabs
-import org.oxycblt.auxio.music.MusicMode
+import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logW
/**
* A representation of a library tab suitable for configuration.
*
- * @param mode The type of list in the home view this instance corresponds to.
+ * @param type The type of list in the home view this instance corresponds to.
* @author Alexander Capehart (OxygenCobalt)
*/
-sealed class Tab(open val mode: MusicMode) {
+sealed class Tab(open val type: MusicType) {
/**
* A visible tab. This will be visible in the home and tab configuration views.
*
- * @param mode The type of list in the home view this instance corresponds to.
+ * @param type The type of list in the home view this instance corresponds to.
*/
- data class Visible(override val mode: MusicMode) : Tab(mode)
+ data class Visible(override val type: MusicType) : Tab(type)
/**
* A visible tab. This will be visible in the tab configuration view, but not in the home view.
*
- * @param mode The type of list in the home view this instance corresponds to.
+ * @param type The type of list in the home view this instance corresponds to.
*/
- data class Invisible(override val mode: MusicMode) : Tab(mode)
+ data class Invisible(override val type: MusicType) : Tab(type)
companion object {
// Like other IO-bound datatypes in Auxio, tabs are stored in a binary format. However, tabs
@@ -67,14 +67,14 @@ sealed class Tab(open val mode: MusicMode) {
*/
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_1100
- /** Maps between the integer code in the tab sequence and it's [MusicMode]. */
+ /** Maps between the integer code in the tab sequence and it's [MusicType]. */
private val MODE_TABLE =
arrayOf(
- MusicMode.SONGS,
- MusicMode.ALBUMS,
- MusicMode.ARTISTS,
- MusicMode.GENRES,
- MusicMode.PLAYLISTS)
+ MusicType.SONGS,
+ MusicType.ALBUMS,
+ MusicType.ARTISTS,
+ MusicType.GENRES,
+ MusicType.PLAYLISTS)
/**
* Convert an array of [Tab]s into it's integer representation.
@@ -84,7 +84,7 @@ sealed class Tab(open val mode: MusicMode) {
*/
fun toIntCode(tabs: Array): Int {
// Like when deserializing, make sure there are no duplicate tabs for whatever reason.
- val distinct = tabs.distinctBy { it.mode }
+ val distinct = tabs.distinctBy { it.type }
if (tabs.size != distinct.size) {
logW(
"Tab sequences should not have duplicates [old: ${tabs.size} new: ${distinct.size}]")
@@ -95,8 +95,8 @@ sealed class Tab(open val mode: MusicMode) {
for (tab in distinct) {
val bin =
when (tab) {
- is Visible -> 1.shl(3) or MODE_TABLE.indexOf(tab.mode)
- is Invisible -> MODE_TABLE.indexOf(tab.mode)
+ is Visible -> 1.shl(3) or MODE_TABLE.indexOf(tab.type)
+ is Invisible -> MODE_TABLE.indexOf(tab.type)
}
sequence = sequence or bin.shl(shift)
@@ -131,7 +131,7 @@ sealed class Tab(open val mode: MusicMode) {
}
// Make sure there are no duplicate tabs
- val distinct = tabs.distinctBy { it.mode }
+ val distinct = tabs.distinctBy { it.type }
if (tabs.size != distinct.size) {
logW(
"Tab sequences should not have duplicates [old: ${tabs.size} new: ${distinct.size}]")
diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt
index 277c0c39b..736b5ba30 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt
@@ -26,7 +26,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemTabBinding
import org.oxycblt.auxio.list.EditClickListListener
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
-import org.oxycblt.auxio.music.MusicMode
+import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logD
@@ -42,7 +42,9 @@ class TabAdapter(private val listener: EditClickListListener) :
private set
override fun getItemCount() = tabs.size
+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = TabViewHolder.from(parent)
+
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
holder.bind(tabs[position], listener)
}
@@ -107,14 +109,14 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) :
fun bind(tab: Tab, listener: EditClickListListener) {
listener.bind(tab, this, dragHandle = binding.tabDragHandle)
binding.tabCheckBox.apply {
- // Update the CheckBox name to align with the mode
+ // Update the CheckBox name to align with the type
setText(
- when (tab.mode) {
- MusicMode.SONGS -> R.string.lbl_songs
- MusicMode.ALBUMS -> R.string.lbl_albums
- MusicMode.ARTISTS -> R.string.lbl_artists
- MusicMode.GENRES -> R.string.lbl_genres
- MusicMode.PLAYLISTS -> R.string.lbl_playlists
+ when (tab.type) {
+ MusicType.SONGS -> R.string.lbl_songs
+ MusicType.ALBUMS -> R.string.lbl_albums
+ MusicType.ARTISTS -> R.string.lbl_artists
+ MusicType.GENRES -> R.string.lbl_genres
+ MusicType.PLAYLISTS -> R.string.lbl_playlists
})
// Unlike in other adapters, we update the checked state alongside
diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt
index c7dadd8d2..57bc73c15 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt
@@ -30,17 +30,18 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogTabsBinding
import org.oxycblt.auxio.home.HomeSettings
import org.oxycblt.auxio.list.EditClickListListener
-import org.oxycblt.auxio.ui.ViewBindingDialogFragment
+import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
import org.oxycblt.auxio.util.logD
/**
- * A [ViewBindingDialogFragment] that allows the user to modify the home [Tab] configuration.
+ * A [ViewBindingMaterialDialogFragment] that allows the user to modify the home [Tab]
+ * configuration.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class TabCustomizeDialog :
- ViewBindingDialogFragment(), EditClickListListener {
+ ViewBindingMaterialDialogFragment(), EditClickListListener {
private val tabAdapter = TabAdapter(this)
private var touchHelper: ItemTouchHelper? = null
@Inject lateinit var homeSettings: HomeSettings
@@ -90,13 +91,13 @@ class TabCustomizeDialog :
override fun onClick(item: Tab, viewHolder: RecyclerView.ViewHolder) {
// We will need the exact index of the tab to update on in order to
// notify the adapter of the change.
- val index = tabAdapter.tabs.indexOfFirst { it.mode == item.mode }
+ val index = tabAdapter.tabs.indexOfFirst { it.type == item.type }
val old = tabAdapter.tabs[index]
val new =
when (old) {
// Invert the visibility of the tab
- is Tab.Visible -> Tab.Invisible(old.mode)
- is Tab.Invisible -> Tab.Visible(old.mode)
+ is Tab.Visible -> Tab.Invisible(old.type)
+ is Tab.Invisible -> Tab.Visible(old.type)
}
logD("Flipping tab visibility [from: $old to: $new]")
tabAdapter.setTab(index, new)
diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt
index 9180c0a35..792755dc7 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt
@@ -83,35 +83,40 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
private val image: ImageView
- data class PlaybackIndicator(
+ private data class PlaybackIndicator(
val view: ImageView,
val playingDrawable: AnimationDrawable,
val pausedDrawable: Drawable
)
+
private val playbackIndicator: PlaybackIndicator?
private val selectionBadge: ImageView?
+ private val sizing: Int
@DimenRes private val iconSizeRes: Int?
- @DimenRes private val cornerRadiusRes: Int?
+ @DimenRes private var cornerRadiusRes: Int?
private var fadeAnimator: ValueAnimator? = null
private val indicatorMatrix = Matrix()
private val indicatorMatrixSrc = RectF()
private val indicatorMatrixDst = RectF()
+ private data class Cover(
+ val songs: Collection,
+ val desc: String,
+ @DrawableRes val errorRes: Int
+ )
+
+ private var currentCover: Cover? = null
+
init {
// Obtain some StyledImageView attributes to use later when theming the custom view.
@SuppressLint("CustomViewStyleable")
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.CoverView)
- val sizing = styledAttrs.getIntOrThrow(R.styleable.CoverView_sizing)
+ sizing = styledAttrs.getIntOrThrow(R.styleable.CoverView_sizing)
iconSizeRes = SIZING_ICON_SIZE[sizing]
- cornerRadiusRes =
- if (uiSettings.roundMode) {
- SIZING_CORNER_RADII[sizing]
- } else {
- null
- }
+ cornerRadiusRes = getCornerRadiusRes()
val playbackIndicatorEnabled =
styledAttrs.getBoolean(R.styleable.CoverView_enablePlaybackIndicator, true)
@@ -161,19 +166,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
playbackIndicator?.run { addView(view) }
- // Add backgrounds to each child for visual consistency
- for (child in children) {
- child.apply {
- // If there are rounded corners, we want to make sure view content will be cropped
- // with it.
- clipToOutline = this != image
- background =
- MaterialShapeDrawable().apply {
- fillColor = context.getColorCompat(R.color.sel_cover_bg)
- setCornerSize(cornerRadiusRes?.let(context::getDimen) ?: 0f)
- }
- }
- }
+ applyBackgroundsToChildren()
// The selection badge has it's own background we don't want overridden, add it after
// all other elements.
@@ -261,6 +254,29 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
}
}
+ private fun getCornerRadiusRes() =
+ if (!isInEditMode && uiSettings.roundMode) {
+ SIZING_CORNER_RADII[sizing]
+ } else {
+ null
+ }
+
+ private fun applyBackgroundsToChildren() {
+ // Add backgrounds to each child for visual consistency
+ for (child in children) {
+ child.apply {
+ // If there are rounded corners, we want to make sure view content will be cropped
+ // with it.
+ clipToOutline = this != image
+ background =
+ MaterialShapeDrawable().apply {
+ fillColor = context.getColorCompat(R.color.sel_cover_bg)
+ setCornerSize(cornerRadiusRes?.let(context::getDimen) ?: 0f)
+ }
+ }
+ }
+ }
+
private fun invalidateRootAlpha() {
alpha = if (isEnabled || isSelected) 1f else 0.5f
}
@@ -401,6 +417,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
CoilUtils.dispose(image)
imageLoader.enqueue(request.build())
contentDescription = desc
+ currentCover = Cover(songs, desc, errorRes)
}
/**
diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt
index 1a9a01b24..232d903a1 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt
@@ -39,7 +39,7 @@ interface ImageSettings : Settings {
interface Listener {
/** Called when [coverMode] changes. */
- fun onCoverModeChanged() {}
+ fun onImageSettingsChanged() {}
}
}
@@ -77,9 +77,10 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context
}
override fun onSettingChanged(key: String, listener: ImageSettings.Listener) {
- if (key == getString(R.string.set_key_cover_mode)) {
- logD("Dispatching cover mode setting change")
- listener.onCoverModeChanged()
+ if (key == getString(R.string.set_key_cover_mode) ||
+ key == getString(R.string.set_key_square_covers)) {
+ logD("Dispatching image setting change")
+ listener.onImageSettingsChanged()
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt
index 537d8e874..899867eb0 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt
@@ -50,7 +50,7 @@ import okio.buffer
import okio.source
import org.oxycblt.auxio.image.CoverMode
import org.oxycblt.auxio.image.ImageSettings
-import org.oxycblt.auxio.list.Sort
+import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logE
diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt
index 7931f63e6..546b03a49 100644
--- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt
@@ -18,25 +18,9 @@
package org.oxycblt.auxio.list
-import android.view.View
-import androidx.annotation.MenuRes
-import androidx.appcompat.widget.PopupMenu
-import androidx.core.view.MenuCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
-import org.oxycblt.auxio.R
-import org.oxycblt.auxio.detail.DetailViewModel
-import org.oxycblt.auxio.list.selection.SelectionFragment
-import org.oxycblt.auxio.music.Album
-import org.oxycblt.auxio.music.Artist
-import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
-import org.oxycblt.auxio.music.Playlist
-import org.oxycblt.auxio.music.Song
-import org.oxycblt.auxio.util.logD
-import org.oxycblt.auxio.util.logW
-import org.oxycblt.auxio.util.share
-import org.oxycblt.auxio.util.showToast
/**
* A Fragment containing a selectable list.
@@ -45,15 +29,6 @@ import org.oxycblt.auxio.util.showToast
*/
abstract class ListFragment :
SelectionFragment(), SelectableListListener {
- protected abstract val detailModel: DetailViewModel
- private var currentMenu: PopupMenu? = null
-
- override fun onDestroyBinding(binding: VB) {
- super.onDestroyBinding(binding)
- currentMenu?.dismiss()
- currentMenu = null
- }
-
/**
* Called when [onClick] is called, but does not result in the item being selected. This more or
* less corresponds to an [onClick] implementation in a non-[ListFragment].
@@ -63,9 +38,9 @@ abstract class ListFragment :
abstract fun onRealClick(item: T)
final override fun onClick(item: T, viewHolder: RecyclerView.ViewHolder) {
- if (selectionModel.selected.value.isNotEmpty()) {
+ if (listModel.selected.value.isNotEmpty()) {
// Map clicking an item to selecting an item when items are already selected.
- selectionModel.select(item)
+ listModel.select(item)
} else {
// Delegate to the concrete implementation when we don't select the item.
onRealClick(item)
@@ -73,307 +48,6 @@ abstract class ListFragment :
}
final override fun onSelect(item: T) {
- selectionModel.select(item)
- }
-
- /**
- * Opens a menu in the context of a [Song]. This menu will be managed by the Fragment and closed
- * when the view is destroyed. If a menu is already opened, this call is ignored.
- *
- * @param anchor The [View] to anchor the menu to.
- * @param menuRes The resource of the menu to load.
- * @param song The [Song] to create the menu for.
- */
- protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, song: Song) {
- logD("Launching new song menu: ${song.name}")
-
- openMenu(anchor, menuRes) {
- setOnMenuItemClickListener {
- when (it.itemId) {
- R.id.action_play_next -> {
- playbackModel.playNext(song)
- requireContext().showToast(R.string.lng_queue_added)
- true
- }
- R.id.action_queue_add -> {
- playbackModel.addToQueue(song)
- requireContext().showToast(R.string.lng_queue_added)
- true
- }
- R.id.action_go_artist -> {
- detailModel.showArtist(song)
- true
- }
- R.id.action_go_album -> {
- detailModel.showAlbum(song.album)
- true
- }
- R.id.action_share -> {
- requireContext().share(song)
- true
- }
- R.id.action_playlist_add -> {
- musicModel.addToPlaylist(song)
- true
- }
- R.id.action_song_detail -> {
- detailModel.showSong(song)
- true
- }
- else -> {
- logW("Unexpected menu item selected")
- false
- }
- }
- }
- }
- }
-
- /**
- * Opens a menu in the context of a [Album]. This menu will be managed by the Fragment and
- * closed when the view is destroyed. If a menu is already opened, this call is ignored.
- *
- * @param anchor The [View] to anchor the menu to.
- * @param menuRes The resource of the menu to load.
- * @param album The [Album] to create the menu for.
- */
- protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, album: Album) {
- logD("Launching new album menu: ${album.name}")
-
- openMenu(anchor, menuRes) {
- setOnMenuItemClickListener {
- when (it.itemId) {
- R.id.action_play -> {
- playbackModel.play(album)
- true
- }
- R.id.action_shuffle -> {
- playbackModel.shuffle(album)
- true
- }
- R.id.action_play_next -> {
- playbackModel.playNext(album)
- requireContext().showToast(R.string.lng_queue_added)
- true
- }
- R.id.action_queue_add -> {
- playbackModel.addToQueue(album)
- requireContext().showToast(R.string.lng_queue_added)
- true
- }
- R.id.action_go_artist -> {
- detailModel.showArtist(album)
- true
- }
- R.id.action_playlist_add -> {
- musicModel.addToPlaylist(album)
- true
- }
- R.id.action_share -> {
- requireContext().share(album)
- true
- }
- else -> {
- logW("Unexpected menu item selected")
- false
- }
- }
- }
- }
- }
-
- /**
- * Opens a menu in the context of a [Artist]. This menu will be managed by the Fragment and
- * closed when the view is destroyed. If a menu is already opened, this call is ignored.
- *
- * @param anchor The [View] to anchor the menu to.
- * @param menuRes The resource of the menu to load.
- * @param artist The [Artist] to create the menu for.
- */
- protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, artist: Artist) {
- logD("Launching new artist menu: ${artist.name}")
-
- openMenu(anchor, menuRes) {
- val playable = artist.songs.isNotEmpty()
- if (!playable) {
- logD("Artist is empty, disabling playback/playlist/share options")
- }
- menu.findItem(R.id.action_play).isEnabled = playable
- menu.findItem(R.id.action_shuffle).isEnabled = playable
- menu.findItem(R.id.action_play_next).isEnabled = playable
- menu.findItem(R.id.action_queue_add).isEnabled = playable
- menu.findItem(R.id.action_playlist_add).isEnabled = playable
- menu.findItem(R.id.action_share).isEnabled = playable
-
- setOnMenuItemClickListener {
- when (it.itemId) {
- R.id.action_play -> {
- playbackModel.play(artist)
- true
- }
- R.id.action_shuffle -> {
- playbackModel.shuffle(artist)
- true
- }
- R.id.action_play_next -> {
- playbackModel.playNext(artist)
- requireContext().showToast(R.string.lng_queue_added)
- true
- }
- R.id.action_queue_add -> {
- playbackModel.addToQueue(artist)
- requireContext().showToast(R.string.lng_queue_added)
- true
- }
- R.id.action_playlist_add -> {
- musicModel.addToPlaylist(artist)
- true
- }
- R.id.action_share -> {
- requireContext().share(artist)
- true
- }
- else -> {
- logW("Unexpected menu item selected")
- false
- }
- }
- }
- }
- }
-
- /**
- * Opens a menu in the context of a [Genre]. This menu will be managed by the Fragment and
- * closed when the view is destroyed. If a menu is already opened, this call is ignored.
- *
- * @param anchor The [View] to anchor the menu to.
- * @param menuRes The resource of the menu to load.
- * @param genre The [Genre] to create the menu for.
- */
- protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, genre: Genre) {
- logD("Launching new genre menu: ${genre.name}")
-
- openMenu(anchor, menuRes) {
- setOnMenuItemClickListener {
- when (it.itemId) {
- R.id.action_play -> {
- playbackModel.play(genre)
- true
- }
- R.id.action_shuffle -> {
- playbackModel.shuffle(genre)
- true
- }
- R.id.action_play_next -> {
- playbackModel.playNext(genre)
- requireContext().showToast(R.string.lng_queue_added)
- true
- }
- R.id.action_queue_add -> {
- playbackModel.addToQueue(genre)
- requireContext().showToast(R.string.lng_queue_added)
- true
- }
- R.id.action_playlist_add -> {
- musicModel.addToPlaylist(genre)
- true
- }
- R.id.action_share -> {
- requireContext().share(genre)
- true
- }
- else -> {
- logW("Unexpected menu item selected")
- false
- }
- }
- }
- }
- }
-
- /**
- * Opens a menu in the context of a [Playlist]. This menu will be managed by the Fragment and
- * closed when the view is destroyed. If a menu is already opened, this call is ignored.
- *
- * @param anchor The [View] to anchor the menu to.
- * @param menuRes The resource of the menu to load.
- * @param playlist The [Playlist] to create the menu for.
- */
- protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, playlist: Playlist) {
- logD("Launching new playlist menu: ${playlist.name}")
-
- openMenu(anchor, menuRes) {
- val playable = playlist.songs.isNotEmpty()
- menu.findItem(R.id.action_play).isEnabled = playable
- menu.findItem(R.id.action_shuffle).isEnabled = playable
- menu.findItem(R.id.action_play_next).isEnabled = playable
- menu.findItem(R.id.action_queue_add).isEnabled = playable
- menu.findItem(R.id.action_share).isEnabled = playable
-
- setOnMenuItemClickListener {
- when (it.itemId) {
- R.id.action_play -> {
- playbackModel.play(playlist)
- true
- }
- R.id.action_shuffle -> {
- playbackModel.shuffle(playlist)
- true
- }
- R.id.action_play_next -> {
- playbackModel.playNext(playlist)
- requireContext().showToast(R.string.lng_queue_added)
- true
- }
- R.id.action_queue_add -> {
- playbackModel.addToQueue(playlist)
- requireContext().showToast(R.string.lng_queue_added)
- true
- }
- R.id.action_rename -> {
- musicModel.renamePlaylist(playlist)
- true
- }
- R.id.action_delete -> {
- musicModel.deletePlaylist(playlist)
- true
- }
- R.id.action_share -> {
- requireContext().share(playlist)
- true
- }
- else -> {
- logW("Unexpected menu item selected")
- false
- }
- }
- }
- }
- }
-
- /**
- * Open a menu. This menu will be managed by the Fragment and closed when the view is destroyed.
- * If a menu is already opened, this call is ignored.
- *
- * @param anchor The [View] to anchor the menu to.
- * @param menuRes The resource of the menu to load.
- * @param block A block that is ran within [PopupMenu] that allows further configuration.
- */
- protected fun openMenu(anchor: View, @MenuRes menuRes: Int, block: PopupMenu.() -> Unit) {
- if (currentMenu != null) {
- logD("Menu already present, not launching")
- return
- }
-
- logD("Opening popup menu menu")
-
- currentMenu =
- PopupMenu(requireContext(), anchor).apply {
- inflate(menuRes)
- MenuCompat.setGroupDividerEnabled(menu, true)
- block()
- setOnDismissListener { currentMenu = null }
- show()
- }
+ listModel.select(item)
}
}
diff --git a/app/src/test/java/org/oxycblt/auxio/util/TestingUtil.kt b/app/src/main/java/org/oxycblt/auxio/list/ListModule.kt
similarity index 68%
rename from app/src/test/java/org/oxycblt/auxio/util/TestingUtil.kt
rename to app/src/main/java/org/oxycblt/auxio/list/ListModule.kt
index 5da90ab54..521bc4283 100644
--- a/app/src/test/java/org/oxycblt/auxio/util/TestingUtil.kt
+++ b/app/src/main/java/org/oxycblt/auxio/list/ListModule.kt
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2023 Auxio Project
- * TestingUtil.kt is part of Auxio.
+ * ListModule.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,13 +16,15 @@
* along with this program. If not, see .
*/
-package org.oxycblt.auxio.util
+package org.oxycblt.auxio.list
-import androidx.lifecycle.ViewModel
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
-private val VM_CLEAR_METHOD =
- ViewModel::class.java.getDeclaredMethod("clear").apply { isAccessible = true }
-
-fun ViewModel.forceClear() {
- VM_CLEAR_METHOD.invoke(this)
+@Module
+@InstallIn(SingletonComponent::class)
+interface ListModule {
+ @Binds fun settings(settings: ListSettingsImpl): ListSettings
}
diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListSettings.kt b/app/src/main/java/org/oxycblt/auxio/list/ListSettings.kt
new file mode 100644
index 000000000..3f3388b73
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/list/ListSettings.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ * ListSettings.kt is part of Auxio.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.auxio.list
+
+import android.content.Context
+import androidx.core.content.edit
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+import org.oxycblt.auxio.R
+import org.oxycblt.auxio.list.sort.Sort
+import org.oxycblt.auxio.settings.Settings
+
+interface ListSettings : Settings {
+ /** The [Sort] mode used in Song lists. */
+ var songSort: Sort
+ /** The [Sort] mode used in Album lists. */
+ var albumSort: Sort
+ /** The [Sort] mode used in Artist lists. */
+ var artistSort: Sort
+ /** The [Sort] mode used in Genre lists. */
+ var genreSort: Sort
+ /** The [Sort] mode used in Playlist lists. */
+ var playlistSort: Sort
+ /** The [Sort] mode used in an Album's Song list. */
+ var albumSongSort: Sort
+ /** The [Sort] mode used in an Artist's Song list. */
+ var artistSongSort: Sort
+ /** The [Sort] mode used in a Genre's Song list. */
+ var genreSongSort: Sort
+}
+
+class ListSettingsImpl @Inject constructor(@ApplicationContext val context: Context) :
+ Settings.Impl(context), ListSettings {
+ override var songSort: Sort
+ get() =
+ Sort.fromIntCode(
+ sharedPreferences.getInt(getString(R.string.set_key_songs_sort), Int.MIN_VALUE))
+ ?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
+ set(value) {
+ sharedPreferences.edit {
+ putInt(getString(R.string.set_key_songs_sort), value.intCode)
+ apply()
+ }
+ }
+
+ override var albumSort: Sort
+ get() =
+ Sort.fromIntCode(
+ sharedPreferences.getInt(getString(R.string.set_key_albums_sort), Int.MIN_VALUE))
+ ?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
+ set(value) {
+ sharedPreferences.edit {
+ putInt(getString(R.string.set_key_albums_sort), value.intCode)
+ apply()
+ }
+ }
+
+ override var artistSort: Sort
+ get() =
+ Sort.fromIntCode(
+ sharedPreferences.getInt(getString(R.string.set_key_artists_sort), Int.MIN_VALUE))
+ ?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
+ set(value) {
+ sharedPreferences.edit {
+ putInt(getString(R.string.set_key_artists_sort), value.intCode)
+ apply()
+ }
+ }
+
+ override var genreSort: Sort
+ get() =
+ Sort.fromIntCode(
+ sharedPreferences.getInt(getString(R.string.set_key_genres_sort), Int.MIN_VALUE))
+ ?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
+ set(value) {
+ sharedPreferences.edit {
+ putInt(getString(R.string.set_key_genres_sort), value.intCode)
+ apply()
+ }
+ }
+
+ override var playlistSort: Sort
+ get() =
+ Sort.fromIntCode(
+ sharedPreferences.getInt(getString(R.string.set_key_playlists_sort), Int.MIN_VALUE))
+ ?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
+ set(value) {
+ sharedPreferences.edit {
+ putInt(getString(R.string.set_key_playlists_sort), value.intCode)
+ apply()
+ }
+ }
+
+ override var albumSongSort: Sort
+ get() =
+ Sort.fromIntCode(
+ sharedPreferences.getInt(
+ getString(R.string.set_key_album_songs_sort), Int.MIN_VALUE))
+ ?: Sort(Sort.Mode.ByDisc, Sort.Direction.ASCENDING)
+ set(value) {
+ sharedPreferences.edit {
+ putInt(getString(R.string.set_key_album_songs_sort), value.intCode)
+ apply()
+ }
+ }
+
+ override var artistSongSort: Sort
+ get() =
+ Sort.fromIntCode(
+ sharedPreferences.getInt(
+ getString(R.string.set_key_artist_songs_sort), Int.MIN_VALUE))
+ ?: Sort(Sort.Mode.ByDate, Sort.Direction.DESCENDING)
+ set(value) {
+ sharedPreferences.edit {
+ putInt(getString(R.string.set_key_artist_songs_sort), value.intCode)
+ apply()
+ }
+ }
+
+ override var genreSongSort: Sort
+ get() =
+ Sort.fromIntCode(
+ sharedPreferences.getInt(
+ getString(R.string.set_key_genre_songs_sort), Int.MIN_VALUE))
+ ?: Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
+ set(value) {
+ sharedPreferences.edit {
+ putInt(getString(R.string.set_key_genre_songs_sort), value.intCode)
+ apply()
+ }
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt
new file mode 100644
index 000000000..e223f439b
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/list/ListViewModel.kt
@@ -0,0 +1,230 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ * ListViewModel.kt is part of Auxio.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.auxio.list
+
+import androidx.annotation.MenuRes
+import androidx.lifecycle.ViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import org.oxycblt.auxio.list.menu.Menu
+import org.oxycblt.auxio.music.Album
+import org.oxycblt.auxio.music.Artist
+import org.oxycblt.auxio.music.Genre
+import org.oxycblt.auxio.music.Music
+import org.oxycblt.auxio.music.MusicParent
+import org.oxycblt.auxio.music.MusicRepository
+import org.oxycblt.auxio.music.Playlist
+import org.oxycblt.auxio.music.Song
+import org.oxycblt.auxio.playback.PlaySong
+import org.oxycblt.auxio.util.Event
+import org.oxycblt.auxio.util.MutableEvent
+import org.oxycblt.auxio.util.logD
+import org.oxycblt.auxio.util.logW
+
+/**
+ * A [ViewModel] that orchestrates menu dialogs and selection state.
+ *
+ * @author Alexander Capehart (OxygenCobalt)
+ */
+@HiltViewModel
+class ListViewModel
+@Inject
+constructor(private val listSettings: ListSettings, private val musicRepository: MusicRepository) :
+ ViewModel(), MusicRepository.UpdateListener {
+ private val _selected = MutableStateFlow(listOf())
+ /** The currently selected items. These are ordered in earliest selected and latest selected. */
+ val selected: StateFlow>
+ get() = _selected
+
+ private val _menu = MutableEvent