Merge branch 'dev' into feature/cover_carousel

This commit is contained in:
Alexander Capehart 2023-08-21 07:50:23 -06:00 committed by GitHub
commit a1abcd7aac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
131 changed files with 2748 additions and 1527 deletions

View file

@ -23,8 +23,8 @@ jobs:
cache: gradle cache: gradle
- name: Grant execute permission for gradlew - name: Grant execute permission for gradlew
run: chmod +x gradlew run: chmod +x gradlew
# - name: Test app with Gradle - name: Test app with Gradle
# run: ./gradlew app:testDebug run: ./gradlew app:testDebug
- name: Build debug APK with Gradle - name: Build debug APK with Gradle
run: ./gradlew app:packageDebug run: ./gradlew app:packageDebug
- name: Upload debug APK artifact - name: Upload debug APK artifact

View file

@ -2,10 +2,17 @@
## dev ## dev
#### What's Fixed
- Fixed app restart being required when changing intelligent sorting
or music separator settings
## 3.2.0
#### What's New #### What's New
- Item and sort menus have been refreshed with a cleaner look - Item and sort menus have been refreshed with a cleaner look
- Added ability to sort playlists - Added ability to sort playlists
- Added option to play song by itself in library/item details - Added option to play song by itself in library/item details
- Added error details when music loading fails
#### What's Improved #### What's Improved
- Made "Add to Playlist" action more prominent in selection toolbar - Made "Add to Playlist" action more prominent in selection toolbar
@ -15,9 +22,6 @@ aspect ratio setting
#### What's Fixed #### What's Fixed
- Playlist detail view now respects playback settings - Playlist detail view now respects playback settings
#### Dev/Meta
- Unified navigation graph
## 3.1.4 ## 3.1.4
#### What's Fixed #### What's Fixed

View file

@ -2,8 +2,8 @@
<h1 align="center"><b>Auxio</b></h1> <h1 align="center"><b>Auxio</b></h1>
<h4 align="center">A simple, rational music player for android.</h4> <h4 align="center">A simple, rational music player for android.</h4>
<p align="center"> <p align="center">
<a href="https://github.com/oxygencobalt/Auxio/releases/tag/v3.1.4"> <a href="https://github.com/oxygencobalt/Auxio/releases/tag/v3.2.0">
<img alt="Latest Version" src="https://img.shields.io/static/v1?label=tag&message=v3.1.4&color=64B5F6&style=flat"> <img alt="Latest Version" src="https://img.shields.io/static/v1?label=tag&message=v3.2.0&color=64B5F6&style=flat">
</a> </a>
<a href="https://github.com/oxygencobalt/Auxio/releases/"> <a href="https://github.com/oxygencobalt/Auxio/releases/">
<img alt="Releases" src="https://img.shields.io/github/downloads/OxygenCobalt/Auxio/total.svg?color=4B95DE&style=flat"> <img alt="Releases" src="https://img.shields.io/github/downloads/OxygenCobalt/Auxio/total.svg?color=4B95DE&style=flat">

View file

@ -21,8 +21,8 @@ android {
defaultConfig { defaultConfig {
applicationId namespace applicationId namespace
versionName "3.1.4" versionName "3.2.0"
versionCode 34 versionCode 35
minSdk 24 minSdk 24
targetSdk 34 targetSdk 34
@ -88,7 +88,7 @@ dependencies {
implementation "androidx.core:core-ktx:1.10.1" implementation "androidx.core:core-ktx:1.10.1"
implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.activity:activity-ktx:1.7.2" implementation "androidx.activity:activity-ktx:1.7.2"
implementation "androidx.fragment:fragment-ktx:1.6.0" implementation "androidx.fragment:fragment-ktx:1.6.1"
// Components // Components
// Deliberately kept on 1.2.1 to prevent a bug where the queue sheet will not collapse on // Deliberately kept on 1.2.1 to prevent a bug where the queue sheet will not collapse on
@ -114,13 +114,11 @@ dependencies {
implementation "androidx.media:media:1.6.0" implementation "androidx.media:media:1.6.0"
// Preferences // Preferences
implementation "androidx.preference:preference-ktx:1.2.0" implementation "androidx.preference:preference-ktx:1.2.1"
// Database // Database
def room_version = '2.6.0-alpha02' def room_version = '2.6.0-alpha03'
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
// I have no clue why, but using KSP breaks the playlist database definition.
//noinspection KaptUsageInsteadOfKsp
ksp "androidx.room:room-compiler:$room_version" ksp "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version" implementation "androidx.room:room-ktx:$room_version"
@ -136,7 +134,7 @@ dependencies {
// Material // Material
// TODO: Exactly figure out the conditions that the 1.7.0 ripple bug occurred so you can just // TODO: Exactly figure out the conditions that the 1.7.0 ripple bug occurred so you can just
// PR a fix. // PR a fix.
implementation "com.google.android.material:material:1.10.0-alpha05" implementation "com.google.android.material:material:1.10.0-alpha06"
// Dependency Injection // Dependency Injection
implementation "com.google.dagger:dagger:$hilt_version" implementation "com.google.dagger:dagger:$hilt_version"
@ -144,9 +142,15 @@ dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version" implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version" kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
// Logging
implementation 'com.jakewharton.timber:timber:5.0.1'
// Testing // Testing
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
testImplementation "junit:junit:4.13.2" testImplementation "junit:junit:4.13.2"
testImplementation "io.mockk:mockk:1.13.7"
testImplementation "org.robolectric:robolectric:4.9"
testImplementation 'androidx.test:core-ktx:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
} }

View file

@ -1,40 +0,0 @@
/*
* Copyright (c) 2023 Auxio Project
* StubTest.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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 <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class StubTest {
// TODO: Make tests
@Test
fun useAppContext() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("org.oxycblt.auxio.debug", appContext.packageName)
}
}

View file

@ -1737,16 +1737,10 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
final boolean shouldHandleGestureInsets = final boolean shouldHandleGestureInsets =
VERSION.SDK_INT >= VERSION_CODES.Q && !isGestureInsetBottomIgnored() && !peekHeightAuto; VERSION.SDK_INT >= VERSION_CODES.Q && !isGestureInsetBottomIgnored() && !peekHeightAuto;
// If were not handling insets at all, don't apply the listener. // MODIFICATION: Fix awful assumption that clients handling edge-to-edge by themselves
if (!paddingBottomSystemWindowInsets // don't need peek height adjustments (Despite the fact that they still likely padding
&& !paddingLeftSystemWindowInsets // the view, just without clipping anything)
&& !paddingRightSystemWindowInsets
&& !marginLeftSystemWindowInsets
&& !marginRightSystemWindowInsets
&& !marginTopSystemWindowInsets
&& !shouldHandleGestureInsets) {
return;
}
ViewUtils.doOnApplyWindowInsets( ViewUtils.doOnApplyWindowInsets(
child, child,
new ViewUtils.OnApplyWindowInsetsListener() { new ViewUtils.OnApplyWindowInsetsListener() {
@ -1758,7 +1752,16 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
Insets mandatoryGestureInsets = Insets mandatoryGestureInsets =
insets.getInsets(WindowInsetsCompat.Type.mandatorySystemGestures()); insets.getInsets(WindowInsetsCompat.Type.mandatorySystemGestures());
insetTop = systemBarInsets.top; // MODIFICATION: Fix second order change of edge-to-edge fix where dialogs will not
// use the nice-looking inset animation and instead blindly shift themselves downwards.
// insetTop = systemBarInsets.top;
// 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)
// Intentionally uses getSystemWindowInsetBottom to apply padding properly when
// adjustResize is used as the windowSoftInputMode.
insetBottom = insets.getSystemWindowInsetBottom();
boolean isRtl = ViewUtils.isLayoutRtl(view); boolean isRtl = ViewUtils.isLayoutRtl(view);
@ -1767,9 +1770,6 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
int rightPadding = view.getPaddingRight(); int rightPadding = view.getPaddingRight();
if (paddingBottomSystemWindowInsets) { if (paddingBottomSystemWindowInsets) {
// Intentionally uses getSystemWindowInsetBottom to apply padding properly when
// adjustResize is used as the windowSoftInputMode.
insetBottom = insets.getSystemWindowInsetBottom();
bottomPadding = initialPadding.bottom + insetBottom; bottomPadding = initialPadding.bottom + insetBottom;
} }
@ -1810,11 +1810,10 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
gestureInsetBottom = mandatoryGestureInsets.bottom; gestureInsetBottom = mandatoryGestureInsets.bottom;
} }
// Don't update the peek height to be above the navigation bar or gestures if these // MODIFICATION: Fix awful assumption that clients handling edge-to-edge by themselves
// flags are off. It means the client is already handling it. // don't need peek height adjustments (Despite the fact that they still likely padding
if (paddingBottomSystemWindowInsets || shouldHandleGestureInsets) { // the view, just without clipping anything)
updatePeekHeight(/* animate= */ false); updatePeekHeight(/* animate= */ false);
}
return insets; return insets;
} }
}); });

View file

@ -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.
*
* <p>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).
*
* <p>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<FrameLayout> 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...
*
* <p>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.
*
* <p>If this function is called from a swipe down interaction, or dismissWithAnimation is false,
* then keep the default behavior.
*
* <p>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<FrameLayout> 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<FrameLayout> 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);
}
}
}

View file

@ -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) {}
}
}

View file

@ -29,6 +29,7 @@ import org.oxycblt.auxio.home.HomeSettings
import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.auxio.image.ImageSettings
import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.ui.UISettings
import timber.log.Timber
/** /**
* A simple, rational music player for android. * A simple, rational music player for android.
@ -44,6 +45,10 @@ class Auxio : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
// Migrate any settings that may have changed in an app update. // Migrate any settings that may have changed in an app update.
imageSettings.migrate() imageSettings.migrate()
playbackSettings.migrate() playbackSettings.migrate()

View file

@ -27,8 +27,6 @@ import androidx.core.view.ViewCompat
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.R as MR import com.google.android.material.R as MR
@ -40,6 +38,7 @@ import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import org.oxycblt.auxio.databinding.FragmentMainBinding import org.oxycblt.auxio.databinding.FragmentMainBinding
import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.detail.Show
import org.oxycblt.auxio.home.HomeViewModel import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.Outer import org.oxycblt.auxio.home.Outer
import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.list.ListViewModel
@ -49,7 +48,9 @@ import org.oxycblt.auxio.playback.OpenPanel
import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior import org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior
import org.oxycblt.auxio.ui.DialogAwareNavigationListener
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.coordinatorLayoutBehavior import org.oxycblt.auxio.util.coordinatorLayoutBehavior
@ -67,9 +68,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class MainFragment : class MainFragment :
ViewBindingFragment<FragmentMainBinding>(), ViewBindingFragment<FragmentMainBinding>(), ViewTreeObserver.OnPreDrawListener {
ViewTreeObserver.OnPreDrawListener,
NavController.OnDestinationChangedListener {
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
private val homeModel: HomeViewModel by activityViewModels() private val homeModel: HomeViewModel by activityViewModels()
private val listModel: ListViewModel by activityViewModels() private val listModel: ListViewModel by activityViewModels()
@ -77,9 +76,9 @@ class MainFragment :
private var sheetBackCallback: SheetBackPressedCallback? = null private var sheetBackCallback: SheetBackPressedCallback? = null
private var detailBackCallback: DetailBackPressedCallback? = null private var detailBackCallback: DetailBackPressedCallback? = null
private var selectionBackCallback: SelectionBackPressedCallback? = null private var selectionBackCallback: SelectionBackPressedCallback? = null
private var selectionNavigationListener: DialogAwareNavigationListener? = null
private var lastInsets: WindowInsets? = null private var lastInsets: WindowInsets? = null
private var elevationNormal = 0f private var elevationNormal = 0f
private var initialNavDestinationChange = true
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -111,6 +110,8 @@ class MainFragment :
val selectionBackCallback = val selectionBackCallback =
SelectionBackPressedCallback(listModel).also { selectionBackCallback = it } SelectionBackPressedCallback(listModel).also { selectionBackCallback = it }
selectionNavigationListener = DialogAwareNavigationListener(listModel::dropSelection)
// --- UI SETUP --- // --- UI SETUP ---
val context = requireActivity() val context = requireActivity()
@ -150,6 +151,11 @@ class MainFragment :
} }
// --- VIEWMODEL SETUP --- // --- 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(detailModel.editedPlaylist, detailBackCallback::invalidateEnabled)
collectImmediately(homeModel.showOuter.flow, ::handleShowOuter) collectImmediately(homeModel.showOuter.flow, ::handleShowOuter)
collectImmediately(listModel.selected, selectionBackCallback::invalidateEnabled) collectImmediately(listModel.selected, selectionBackCallback::invalidateEnabled)
@ -162,8 +168,8 @@ class MainFragment :
val binding = requireBinding() val binding = requireBinding()
// Once we add the destination change callback, we will receive another initialization call, // Once we add the destination change callback, we will receive another initialization call,
// so handle that by resetting the flag. // so handle that by resetting the flag.
initialNavDestinationChange = false requireNotNull(selectionNavigationListener) { "NavigationListener was not available" }
binding.exploreNavHost.findNavController().addOnDestinationChangedListener(this) .attach(binding.exploreNavHost.findNavController())
// Listener could still reasonably fire even if we clear the binding, attach/detach // Listener could still reasonably fire even if we clear the binding, attach/detach
// our pre-draw listener our listener in onStart/onStop respectively. // our pre-draw listener our listener in onStart/onStop respectively.
binding.playbackSheet.viewTreeObserver.addOnPreDrawListener(this@MainFragment) binding.playbackSheet.viewTreeObserver.addOnPreDrawListener(this@MainFragment)
@ -184,7 +190,8 @@ class MainFragment :
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
val binding = requireBinding() val binding = requireBinding()
binding.exploreNavHost.findNavController().removeOnDestinationChangedListener(this) requireNotNull(selectionNavigationListener) { "NavigationListener was not available" }
.release(binding.exploreNavHost.findNavController())
binding.playbackSheet.viewTreeObserver.removeOnPreDrawListener(this) binding.playbackSheet.viewTreeObserver.removeOnPreDrawListener(this)
} }
@ -193,6 +200,7 @@ class MainFragment :
sheetBackCallback = null sheetBackCallback = null
detailBackCallback = null detailBackCallback = null
selectionBackCallback = null selectionBackCallback = null
selectionNavigationListener = null
} }
override fun onPreDraw(): Boolean { override fun onPreDraw(): Boolean {
@ -286,19 +294,18 @@ class MainFragment :
return true return true
} }
override fun onDestinationChanged( private fun handleShow(show: Show?) {
controller: NavController, when (show) {
destination: NavDestination, is Show.SongAlbumDetails,
arguments: Bundle? is Show.ArtistDetails,
) { is Show.AlbumDetails -> playbackModel.openMain()
// Drop the initial call by NavController that simply provides us with the current is Show.SongDetails,
// destination. This would cause the selection state to be lost every time the device is Show.SongArtistDecision,
// rotates. is Show.AlbumArtistDecision,
if (!initialNavDestinationChange) { is Show.GenreDetails,
initialNavDestinationChange = true is Show.PlaylistDetails,
return null -> {}
} }
listModel.dropSelection()
} }
private fun handleShowOuter(outer: Outer?) { private fun handleShowOuter(outer: Outer?) {

View file

@ -101,7 +101,7 @@ class AlbumDetailFragment :
setNavigationOnClickListener { findNavController().navigateUp() } setNavigationOnClickListener { findNavController().navigateUp() }
overrideOnOverflowMenuClick { overrideOnOverflowMenuClick {
listModel.openMenu( listModel.openMenu(
R.menu.item_detail_album, unlikelyToBeNull(detailModel.currentAlbum.value)) R.menu.detail_album, unlikelyToBeNull(detailModel.currentAlbum.value))
} }
} }
@ -145,7 +145,7 @@ class AlbumDetailFragment :
} }
override fun onOpenMenu(item: Song) { override fun onOpenMenu(item: Song) {
listModel.openMenu(R.menu.item_album_song, item, detailModel.playInAlbumWith) listModel.openMenu(R.menu.album_song, item, detailModel.playInAlbumWith)
} }
override fun onPlay() { override fun onPlay() {
@ -243,6 +243,7 @@ class AlbumDetailFragment :
when (menu) { when (menu) {
is Menu.ForSong -> AlbumDetailFragmentDirections.openSongMenu(menu.parcel) is Menu.ForSong -> AlbumDetailFragmentDirections.openSongMenu(menu.parcel)
is Menu.ForAlbum -> AlbumDetailFragmentDirections.openAlbumMenu(menu.parcel) is Menu.ForAlbum -> AlbumDetailFragmentDirections.openAlbumMenu(menu.parcel)
is Menu.ForSelection -> AlbumDetailFragmentDirections.openSelectionMenu(menu.parcel)
is Menu.ForArtist, is Menu.ForArtist,
is Menu.ForGenre, is Menu.ForGenre,
is Menu.ForPlaylist -> error("Unexpected menu $menu") is Menu.ForPlaylist -> error("Unexpected menu $menu")

View file

@ -100,7 +100,7 @@ class ArtistDetailFragment :
setOnMenuItemClickListener(this@ArtistDetailFragment) setOnMenuItemClickListener(this@ArtistDetailFragment)
overrideOnOverflowMenuClick { overrideOnOverflowMenuClick {
listModel.openMenu( listModel.openMenu(
R.menu.item_detail_parent, unlikelyToBeNull(detailModel.currentArtist.value)) R.menu.detail_parent, unlikelyToBeNull(detailModel.currentArtist.value))
} }
} }
@ -152,9 +152,8 @@ class ArtistDetailFragment :
override fun onOpenMenu(item: Music) { override fun onOpenMenu(item: Music) {
when (item) { when (item) {
is Song -> is Song -> listModel.openMenu(R.menu.artist_song, item, detailModel.playInArtistWith)
listModel.openMenu(R.menu.item_artist_song, item, detailModel.playInArtistWith) is Album -> listModel.openMenu(R.menu.artist_album, item)
is Album -> listModel.openMenu(R.menu.item_artist_album, item)
else -> error("Unexpected datatype: ${item::class.simpleName}") else -> error("Unexpected datatype: ${item::class.simpleName}")
} }
} }
@ -222,8 +221,16 @@ class ArtistDetailFragment :
.navigateSafe(ArtistDetailFragmentDirections.showArtist(show.artist.uid)) .navigateSafe(ArtistDetailFragmentDirections.showArtist(show.artist.uid))
} }
} }
is Show.SongArtistDecision, is Show.SongArtistDecision -> {
is Show.AlbumArtistDecision, 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.GenreDetails,
is Show.PlaylistDetails -> { is Show.PlaylistDetails -> {
error("Unexpected show command $show") error("Unexpected show command $show")
@ -239,6 +246,8 @@ class ArtistDetailFragment :
is Menu.ForSong -> ArtistDetailFragmentDirections.openSongMenu(menu.parcel) is Menu.ForSong -> ArtistDetailFragmentDirections.openSongMenu(menu.parcel)
is Menu.ForAlbum -> ArtistDetailFragmentDirections.openAlbumMenu(menu.parcel) is Menu.ForAlbum -> ArtistDetailFragmentDirections.openAlbumMenu(menu.parcel)
is Menu.ForArtist -> ArtistDetailFragmentDirections.openArtistMenu(menu.parcel) is Menu.ForArtist -> ArtistDetailFragmentDirections.openArtistMenu(menu.parcel)
is Menu.ForSelection ->
ArtistDetailFragmentDirections.openSelectionMenu(menu.parcel)
is Menu.ForGenre, is Menu.ForGenre,
is Menu.ForPlaylist -> error("Unexpected menu $menu") is Menu.ForPlaylist -> error("Unexpected menu $menu")
} }

View file

@ -98,7 +98,7 @@ class GenreDetailFragment :
setOnMenuItemClickListener(this@GenreDetailFragment) setOnMenuItemClickListener(this@GenreDetailFragment)
overrideOnOverflowMenuClick { overrideOnOverflowMenuClick {
listModel.openMenu( listModel.openMenu(
R.menu.item_detail_parent, unlikelyToBeNull(detailModel.currentGenre.value)) R.menu.detail_parent, unlikelyToBeNull(detailModel.currentGenre.value))
} }
} }
@ -150,8 +150,8 @@ class GenreDetailFragment :
override fun onOpenMenu(item: Music) { override fun onOpenMenu(item: Music) {
when (item) { when (item) {
is Artist -> listModel.openMenu(R.menu.item_parent, item) is Artist -> listModel.openMenu(R.menu.parent, item)
is Song -> listModel.openMenu(R.menu.item_song, item, detailModel.playInGenreWith) is Song -> listModel.openMenu(R.menu.song, item, detailModel.playInGenreWith)
else -> error("Unexpected datatype: ${item::class.simpleName}") else -> error("Unexpected datatype: ${item::class.simpleName}")
} }
} }
@ -240,6 +240,7 @@ class GenreDetailFragment :
is Menu.ForSong -> GenreDetailFragmentDirections.openSongMenu(menu.parcel) is Menu.ForSong -> GenreDetailFragmentDirections.openSongMenu(menu.parcel)
is Menu.ForArtist -> GenreDetailFragmentDirections.openArtistMenu(menu.parcel) is Menu.ForArtist -> GenreDetailFragmentDirections.openArtistMenu(menu.parcel)
is Menu.ForGenre -> GenreDetailFragmentDirections.openGenreMenu(menu.parcel) is Menu.ForGenre -> GenreDetailFragmentDirections.openGenreMenu(menu.parcel)
is Menu.ForSelection -> GenreDetailFragmentDirections.openSelectionMenu(menu.parcel)
is Menu.ForAlbum, is Menu.ForAlbum,
is Menu.ForPlaylist -> error("Unexpected menu $menu") is Menu.ForPlaylist -> error("Unexpected menu $menu")
} }

View file

@ -20,9 +20,8 @@ package org.oxycblt.auxio.detail
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
@ -51,6 +50,7 @@ import org.oxycblt.auxio.music.PlaylistDecision
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackDecision import org.oxycblt.auxio.playback.PlaybackDecision
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.DialogAwareNavigationListener
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -68,8 +68,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
class PlaylistDetailFragment : class PlaylistDetailFragment :
ListFragment<Song, FragmentDetailBinding>(), ListFragment<Song, FragmentDetailBinding>(),
DetailHeaderAdapter.Listener, DetailHeaderAdapter.Listener,
PlaylistDetailListAdapter.Listener, PlaylistDetailListAdapter.Listener {
NavController.OnDestinationChangedListener {
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
override val listModel: ListViewModel by activityViewModels() override val listModel: ListViewModel by activityViewModels()
override val musicModel: MusicViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels()
@ -80,7 +79,7 @@ class PlaylistDetailFragment :
private val playlistHeaderAdapter = PlaylistDetailHeaderAdapter(this) private val playlistHeaderAdapter = PlaylistDetailHeaderAdapter(this)
private val playlistListAdapter = PlaylistDetailListAdapter(this) private val playlistListAdapter = PlaylistDetailListAdapter(this)
private var touchHelper: ItemTouchHelper? = null private var touchHelper: ItemTouchHelper? = null
private var initialNavDestinationChange = false private var editNavigationListener: DialogAwareNavigationListener? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -98,14 +97,15 @@ class PlaylistDetailFragment :
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
editNavigationListener = DialogAwareNavigationListener(detailModel::dropPlaylistEdit)
// --- UI SETUP --- // --- UI SETUP ---
binding.detailNormalToolbar.apply { binding.detailNormalToolbar.apply {
setNavigationOnClickListener { findNavController().navigateUp() } setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener(this@PlaylistDetailFragment) setOnMenuItemClickListener(this@PlaylistDetailFragment)
overrideOnOverflowMenuClick { overrideOnOverflowMenuClick {
listModel.openMenu( listModel.openMenu(
R.menu.item_detail_playlist, R.menu.detail_playlist, unlikelyToBeNull(detailModel.currentPlaylist.value))
unlikelyToBeNull(detailModel.currentPlaylist.value))
} }
} }
@ -148,17 +148,31 @@ class PlaylistDetailFragment :
collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision) 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() { override fun onStart() {
super.onStart() super.onStart()
// Once we add the destination change callback, we will receive another initialization call, // Once we add the destination change callback, we will receive another initialization call,
// so handle that by resetting the flag. // so handle that by resetting the flag.
initialNavDestinationChange = false requireNotNull(editNavigationListener) { "NavigationListener was not available" }
findNavController().addOnDestinationChangedListener(this) .attach(findNavController())
} }
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
findNavController().removeOnDestinationChangedListener(this) requireNotNull(editNavigationListener) { "NavigationListener was not available" }
.release(findNavController())
} }
override fun onDestroyBinding(binding: FragmentDetailBinding) { override fun onDestroyBinding(binding: FragmentDetailBinding) {
@ -169,26 +183,7 @@ class PlaylistDetailFragment :
// Avoid possible race conditions that could cause a bad replace instruction to be consumed // 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. // during list initialization and crash the app. Could happen if the user is fast enough.
detailModel.playlistSongInstructions.consume() detailModel.playlistSongInstructions.consume()
} editNavigationListener = null
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
}
if (destination.id != R.id.playlist_detail_fragment &&
destination.id != R.id.playlist_song_sort_dialog) {
// Drop any pending playlist edits when navigating away. This could actually happen
// if the user is quick enough.
detailModel.dropPlaylistEdit()
}
} }
override fun onRealClick(item: Song) { override fun onRealClick(item: Song) {
@ -200,7 +195,7 @@ class PlaylistDetailFragment :
} }
override fun onOpenMenu(item: Song) { override fun onOpenMenu(item: Song) {
listModel.openMenu(R.menu.item_playlist_song, item, detailModel.playInPlaylistWith) listModel.openMenu(R.menu.playlist_song, item, detailModel.playInPlaylistWith)
} }
override fun onPlay() { override fun onPlay() {
@ -302,6 +297,8 @@ class PlaylistDetailFragment :
is Menu.ForSong -> PlaylistDetailFragmentDirections.openSongMenu(menu.parcel) is Menu.ForSong -> PlaylistDetailFragmentDirections.openSongMenu(menu.parcel)
is Menu.ForPlaylist -> is Menu.ForPlaylist ->
PlaylistDetailFragmentDirections.openPlaylistMenu(menu.parcel) PlaylistDetailFragmentDirections.openPlaylistMenu(menu.parcel)
is Menu.ForSelection ->
PlaylistDetailFragmentDirections.openSelectionMenu(menu.parcel)
is Menu.ForArtist, is Menu.ForArtist,
is Menu.ForAlbum, is Menu.ForAlbum,
is Menu.ForGenre -> error("Unexpected menu $menu") is Menu.ForGenre -> error("Unexpected menu $menu")

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<DialogErrorDetailsBinding>() {
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"
}
}

View file

@ -51,7 +51,7 @@ constructor(
// Apply the new configuration possibly set in flipTo. This should occur even if // Apply the new configuration possibly set in flipTo. This should occur even if
// a flip was canceled by a hide. // a flip was canceled by a hide.
pendingConfig?.run { pendingConfig?.run {
this@FlipFloatingActionButton.logD("Applying pending configuration") logD("Applying pending configuration")
setImageResource(iconRes) setImageResource(iconRes)
contentDescription = context.getString(contentDescriptionRes) contentDescription = context.getString(contentDescriptionRes)
setOnClickListener(clickListener) setOnClickListener(clickListener)

View file

@ -330,7 +330,7 @@ class HomeFragment :
} }
} }
private fun setupCompleteState(binding: FragmentHomeBinding, error: Throwable?) { private fun setupCompleteState(binding: FragmentHomeBinding, error: Exception?) {
if (error == null) { if (error == null) {
logD("Received ok response") logD("Received ok response")
binding.homeFab.show() binding.homeFab.show()
@ -342,13 +342,13 @@ class HomeFragment :
val context = requireContext() val context = requireContext()
binding.homeIndexingContainer.visibility = View.VISIBLE binding.homeIndexingContainer.visibility = View.VISIBLE
binding.homeIndexingProgress.visibility = View.INVISIBLE binding.homeIndexingProgress.visibility = View.INVISIBLE
binding.homeIndexingActions.visibility = View.VISIBLE
when (error) { when (error) {
is NoAudioPermissionException -> { is NoAudioPermissionException -> {
logD("Showing permission prompt") logD("Showing permission prompt")
binding.homeIndexingStatus.text = context.getString(R.string.err_no_perms) binding.homeIndexingStatus.text = context.getString(R.string.err_no_perms)
// Configure the action to act as a permission launcher. // Configure the action to act as a permission launcher.
binding.homeIndexingAction.apply { binding.homeIndexingTry.apply {
visibility = View.VISIBLE
text = context.getString(R.string.lbl_grant) text = context.getString(R.string.lbl_grant)
setOnClickListener { setOnClickListener {
requireNotNull(storagePermissionLauncher) { requireNotNull(storagePermissionLauncher) {
@ -357,26 +357,34 @@ class HomeFragment :
.launch(PERMISSION_READ_AUDIO) .launch(PERMISSION_READ_AUDIO)
} }
} }
binding.homeIndexingMore.visibility = View.GONE
} }
is NoMusicException -> { is NoMusicException -> {
logD("Showing no music error") logD("Showing no music error")
binding.homeIndexingStatus.text = context.getString(R.string.err_no_music) binding.homeIndexingStatus.text = context.getString(R.string.err_no_music)
// Configure the action to act as a reload trigger. // Configure the action to act as a reload trigger.
binding.homeIndexingAction.apply { binding.homeIndexingTry.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
text = context.getString(R.string.lbl_retry) text = context.getString(R.string.lbl_retry)
setOnClickListener { musicModel.refresh() } setOnClickListener { musicModel.refresh() }
} }
binding.homeIndexingMore.visibility = View.GONE
} }
else -> { else -> {
logD("Showing generic error") logD("Showing generic error")
binding.homeIndexingStatus.text = context.getString(R.string.err_index_failed) binding.homeIndexingStatus.text = context.getString(R.string.err_index_failed)
// Configure the action to act as a reload trigger. // Configure the action to act as a reload trigger.
binding.homeIndexingAction.apply { binding.homeIndexingTry.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
text = context.getString(R.string.lbl_retry) text = context.getString(R.string.lbl_retry)
setOnClickListener { musicModel.rescan() } setOnClickListener { musicModel.rescan() }
} }
binding.homeIndexingMore.apply {
visibility = View.VISIBLE
setOnClickListener {
findNavController().navigateSafe(HomeFragmentDirections.reportError(error))
}
}
} }
} }
} }
@ -385,7 +393,7 @@ class HomeFragment :
// Remove all content except for the progress indicator. // Remove all content except for the progress indicator.
binding.homeIndexingContainer.visibility = View.VISIBLE binding.homeIndexingContainer.visibility = View.VISIBLE
binding.homeIndexingProgress.visibility = View.VISIBLE binding.homeIndexingProgress.visibility = View.VISIBLE
binding.homeIndexingAction.visibility = View.INVISIBLE binding.homeIndexingActions.visibility = View.INVISIBLE
when (progress) { when (progress) {
is IndexingProgress.Indeterminate -> { is IndexingProgress.Indeterminate -> {
@ -501,6 +509,7 @@ class HomeFragment :
is Menu.ForArtist -> HomeFragmentDirections.openArtistMenu(menu.parcel) is Menu.ForArtist -> HomeFragmentDirections.openArtistMenu(menu.parcel)
is Menu.ForGenre -> HomeFragmentDirections.openGenreMenu(menu.parcel) is Menu.ForGenre -> HomeFragmentDirections.openGenreMenu(menu.parcel)
is Menu.ForPlaylist -> HomeFragmentDirections.openPlaylistMenu(menu.parcel) is Menu.ForPlaylist -> HomeFragmentDirections.openPlaylistMenu(menu.parcel)
is Menu.ForSelection -> HomeFragmentDirections.openSelectionMenu(menu.parcel)
} }
findNavController().navigateSafe(directions) findNavController().navigateSafe(directions)
} }

View file

@ -140,7 +140,7 @@ class AlbumListFragment :
} }
override fun onOpenMenu(item: Album) { override fun onOpenMenu(item: Album) {
listModel.openMenu(R.menu.item_album, item) listModel.openMenu(R.menu.album, item)
} }
private fun updateAlbums(albums: List<Album>) { private fun updateAlbums(albums: List<Album>) {

View file

@ -116,7 +116,7 @@ class ArtistListFragment :
} }
override fun onOpenMenu(item: Artist) { override fun onOpenMenu(item: Artist) {
listModel.openMenu(R.menu.item_parent, item) listModel.openMenu(R.menu.parent, item)
} }
private fun updateArtists(artists: List<Artist>) { private fun updateArtists(artists: List<Artist>) {

View file

@ -115,7 +115,7 @@ class GenreListFragment :
} }
override fun onOpenMenu(item: Genre) { override fun onOpenMenu(item: Genre) {
listModel.openMenu(R.menu.item_parent, item) listModel.openMenu(R.menu.parent, item)
} }
private fun updateGenres(genres: List<Genre>) { private fun updateGenres(genres: List<Genre>) {

View file

@ -113,7 +113,7 @@ class PlaylistListFragment :
} }
override fun onOpenMenu(item: Playlist) { override fun onOpenMenu(item: Playlist) {
listModel.openMenu(R.menu.item_playlist, item) listModel.openMenu(R.menu.playlist, item)
} }
private fun updatePlaylists(playlists: List<Playlist>) { private fun updatePlaylists(playlists: List<Playlist>) {

View file

@ -139,7 +139,7 @@ class SongListFragment :
} }
override fun onOpenMenu(item: Song) { override fun onOpenMenu(item: Song) {
listModel.openMenu(R.menu.item_song, item, homeModel.playWith) listModel.openMenu(R.menu.song, item, homeModel.playWith)
} }
private fun updateSongs(songs: List<Song>) { private fun updateSongs(songs: List<Song>) {

View file

@ -114,10 +114,8 @@ constructor(private val listSettings: ListSettings, private val musicRepository:
* *
* @return A list of [Song]s collated from each item selected. * @return A list of [Song]s collated from each item selected.
*/ */
fun takeSelection(): List<Song> { fun peekSelection() =
logD("Taking selection") _selected.value.flatMap {
return _selected.value
.flatMap {
when (it) { when (it) {
is Song -> listOf(it) is Song -> listOf(it)
is Album -> listSettings.albumSongSort.songs(it.songs) is Album -> listSettings.albumSongSort.songs(it.songs)
@ -126,7 +124,15 @@ constructor(private val listSettings: ListSettings, private val musicRepository:
is Playlist -> it.songs is Playlist -> it.songs
} }
} }
.also { _selected.value = listOf() }
/**
* Clear the current selection and return it.
*
* @return A list of [Song]s collated from each item selected.
*/
fun takeSelection(): List<Song> {
logD("Taking selection")
return peekSelection().also { _selected.value = listOf() }
} }
/** /**
@ -201,6 +207,18 @@ constructor(private val listSettings: ListSettings, private val musicRepository:
openImpl(Menu.ForPlaylist(menuRes, playlist)) openImpl(Menu.ForPlaylist(menuRes, playlist))
} }
/**
* Open a menu for a [Song] selection. This is not a popup menu, instead actually a dialog of
* menu options with additional information.
*
* @param menuRes The resource of the menu to use.
* @param songs The [Song] selection to show.
*/
fun openMenu(@MenuRes menuRes: Int, songs: List<Song>) {
logD("Opening menu for ${songs.size} songs")
openImpl(Menu.ForSelection(menuRes, songs))
}
private fun openImpl(menu: Menu) { private fun openImpl(menu: Menu) {
val existing = _menu.flow.value val existing = _menu.flow.value
if (existing != null) { if (existing != null) {

View file

@ -26,7 +26,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
/** /**
@ -48,6 +48,9 @@ abstract class SelectionFragment<VB : ViewBinding> :
// Add cancel and menu item listeners to manage what occurs with the selection. // Add cancel and menu item listeners to manage what occurs with the selection.
setNavigationOnClickListener { listModel.dropSelection() } setNavigationOnClickListener { listModel.dropSelection() }
setOnMenuItemClickListener(this@SelectionFragment) setOnMenuItemClickListener(this@SelectionFragment)
overrideOnOverflowMenuClick {
listModel.openMenu(R.menu.selection, listModel.peekSelection())
}
} }
} }
@ -67,23 +70,6 @@ abstract class SelectionFragment<VB : ViewBinding> :
musicModel.addToPlaylist(listModel.takeSelection()) musicModel.addToPlaylist(listModel.takeSelection())
true true
} }
R.id.action_selection_queue_add -> {
playbackModel.addToQueue(listModel.takeSelection())
requireContext().showToast(R.string.lng_queue_added)
true
}
R.id.action_selection_play -> {
playbackModel.play(listModel.takeSelection())
true
}
R.id.action_selection_shuffle -> {
playbackModel.shuffle(listModel.takeSelection())
true
}
R.id.action_selection_share -> {
requireContext().share(listModel.takeSelection())
true
}
else -> false else -> false
} }

View file

@ -99,4 +99,11 @@ sealed interface Menu {
@Parcelize data class Parcel(val res: Int, val playlistUid: Music.UID) : Menu.Parcel @Parcelize data class Parcel(val res: Int, val playlistUid: Music.UID) : Menu.Parcel
} }
class ForSelection(@MenuRes override val res: Int, val songs: List<Song>) : Menu {
override val parcel: Parcel
get() = Parcel(res, songs.map { it.uid })
@Parcelize data class Parcel(val res: Int, val songUids: List<Music.UID>) : Menu.Parcel
}
} }

View file

@ -34,6 +34,7 @@ import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.share import org.oxycblt.auxio.util.share
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
@ -78,10 +79,10 @@ class SongMenuDialogFragment : MenuDialogFragment<Menu.ForSong>() {
playbackModel.addToQueue(menu.song) playbackModel.addToQueue(menu.song)
requireContext().showToast(R.string.lng_queue_added) requireContext().showToast(R.string.lng_queue_added)
} }
R.id.action_playlist_add -> musicModel.addToPlaylist(menu.song)
R.id.action_artist_details -> detailModel.showArtist(menu.song) R.id.action_artist_details -> detailModel.showArtist(menu.song)
R.id.action_album_details -> detailModel.showAlbum(menu.song.album) R.id.action_album_details -> detailModel.showAlbum(menu.song.album)
R.id.action_share -> requireContext().share(menu.song) R.id.action_share -> requireContext().share(menu.song)
R.id.action_playlist_add -> musicModel.addToPlaylist(menu.song)
R.id.action_detail -> detailModel.showSong(menu.song) R.id.action_detail -> detailModel.showSong(menu.song)
else -> error("Unexpected menu item selected $item") else -> error("Unexpected menu item selected $item")
} }
@ -321,3 +322,51 @@ class PlaylistMenuDialogFragment : MenuDialogFragment<Menu.ForPlaylist>() {
} }
} }
} }
/**
* [MenuDialogFragment] implementation for a [Song] selection.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class SelectionMenuDialogFragment : MenuDialogFragment<Menu.ForSelection>() {
override val menuModel: MenuViewModel by activityViewModels()
override val listModel: ListViewModel by activityViewModels()
private val musicModel: MusicViewModel by activityViewModels()
private val playbackModel: PlaybackViewModel by activityViewModels()
private val args: SelectionMenuDialogFragmentArgs by navArgs()
override val parcel
get() = args.parcel
// Nothing to disable in song menus.
override fun getDisabledItemIds(menu: Menu.ForSelection) = setOf<Int>()
override fun updateMenu(binding: DialogMenuBinding, menu: Menu.ForSelection) {
binding.menuCover.bind(
menu.songs, getString(R.string.desc_selection_image), R.drawable.ic_song_24)
binding.menuType.text = getString(R.string.lbl_selection)
binding.menuName.text =
requireContext().getPlural(R.plurals.fmt_song_count, menu.songs.size)
binding.menuInfo.text = menu.songs.sumOf { it.durationMs }.formatDurationMs(true)
}
override fun onClick(item: MenuItem, menu: Menu.ForSelection) {
listModel.dropSelection()
when (item.itemId) {
R.id.action_play -> playbackModel.play(menu.songs)
R.id.action_shuffle -> playbackModel.shuffle(menu.songs)
R.id.action_play_next -> {
playbackModel.playNext(menu.songs)
requireContext().showToast(R.string.lng_queue_added)
}
R.id.action_queue_add -> {
playbackModel.addToQueue(menu.songs)
requireContext().showToast(R.string.lng_queue_added)
}
R.id.action_playlist_add -> musicModel.addToPlaylist(menu.songs)
R.id.action_share -> requireContext().share(menu.songs)
else -> error("Unexpected menu item selected $item")
}
}
}

View file

@ -66,6 +66,7 @@ class MenuViewModel @Inject constructor(private val musicRepository: MusicReposi
is Menu.ForArtist.Parcel -> unpackArtistParcel(parcel) is Menu.ForArtist.Parcel -> unpackArtistParcel(parcel)
is Menu.ForGenre.Parcel -> unpackGenreParcel(parcel) is Menu.ForGenre.Parcel -> unpackGenreParcel(parcel)
is Menu.ForPlaylist.Parcel -> unpackPlaylistParcel(parcel) is Menu.ForPlaylist.Parcel -> unpackPlaylistParcel(parcel)
is Menu.ForSelection.Parcel -> unpackSelectionParcel(parcel)
} }
private fun unpackSongParcel(parcel: Menu.ForSong.Parcel): Menu.ForSong? { private fun unpackSongParcel(parcel: Menu.ForSong.Parcel): Menu.ForSong? {
@ -94,4 +95,10 @@ class MenuViewModel @Inject constructor(private val musicRepository: MusicReposi
val playlist = musicRepository.userLibrary?.findPlaylist(parcel.playlistUid) ?: return null val playlist = musicRepository.userLibrary?.findPlaylist(parcel.playlistUid) ?: return null
return Menu.ForPlaylist(parcel.res, playlist) return Menu.ForPlaylist(parcel.res, playlist)
} }
private fun unpackSelectionParcel(parcel: Menu.ForSelection.Parcel): Menu.ForSelection? {
val deviceLibrary = musicRepository.deviceLibrary ?: return null
val songs = parcel.songUids.mapNotNull(deviceLibrary::findSong)
return Menu.ForSelection(parcel.res, songs)
}
} }

View file

@ -47,7 +47,7 @@ sealed interface IndexingState {
* @param error If music loading has failed, the error that occurred will be here. Otherwise, it * @param error If music loading has failed, the error that occurred will be here. Otherwise, it
* will be null. * will be null.
*/ */
data class Completed(val error: Throwable?) : IndexingState data class Completed(val error: Exception?) : IndexingState
} }
/** /**

View file

@ -223,7 +223,8 @@ constructor(
private val mediaStoreExtractor: MediaStoreExtractor, private val mediaStoreExtractor: MediaStoreExtractor,
private val tagExtractor: TagExtractor, private val tagExtractor: TagExtractor,
private val deviceLibraryFactory: DeviceLibrary.Factory, private val deviceLibraryFactory: DeviceLibrary.Factory,
private val userLibraryFactory: UserLibrary.Factory private val userLibraryFactory: UserLibrary.Factory,
private val musicSettings: MusicSettings
) : MusicRepository { ) : MusicRepository {
private val updateListeners = mutableListOf<MusicRepository.UpdateListener>() private val updateListeners = mutableListOf<MusicRepository.UpdateListener>()
private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>() private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>()
@ -371,6 +372,7 @@ constructor(
// parallel. // parallel.
logD("Starting MediaStore query") logD("Starting MediaStore query")
emitIndexingProgress(IndexingProgress.Indeterminate) emitIndexingProgress(IndexingProgress.Indeterminate)
val mediaStoreQueryJob = val mediaStoreQueryJob =
worker.scope.async { worker.scope.async {
val query = val query =

View file

@ -43,7 +43,7 @@ interface MusicSettings : Settings<MusicSettings.Listener> {
/** Whether to be actively watching for changes in the music library. */ /** Whether to be actively watching for changes in the music library. */
val shouldBeObserving: Boolean val shouldBeObserving: Boolean
/** A [String] of characters representing the desired characters to denote multi-value tags. */ /** A [String] of characters representing the desired characters to denote multi-value tags. */
var multiValueSeparators: String var separators: String
/** Whether to enable more advanced sorting by articles and numbers. */ /** Whether to enable more advanced sorting by articles and numbers. */
val intelligentSorting: Boolean val intelligentSorting: Boolean
@ -85,7 +85,7 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context
override val shouldBeObserving: Boolean override val shouldBeObserving: Boolean
get() = sharedPreferences.getBoolean(getString(R.string.set_key_observing), false) get() = sharedPreferences.getBoolean(getString(R.string.set_key_observing), false)
override var multiValueSeparators: String override var separators: String
// Differ from convention and store a string of separator characters instead of an int // Differ from convention and store a string of separator characters instead of an int
// code. This makes it easier to use and more extendable. // code. This makes it easier to use and more extendable.
get() = sharedPreferences.getString(getString(R.string.set_key_separators), "") ?: "" get() = sharedPreferences.getString(getString(R.string.set_key_separators), "") ?: ""

View file

@ -63,9 +63,9 @@ data class CachedSong(
/** @see RawSong */ /** @see RawSong */
var durationMs: Long, var durationMs: Long,
/** @see RawSong.replayGainTrackAdjustment */ /** @see RawSong.replayGainTrackAdjustment */
val replayGainTrackAdjustment: Float?, val replayGainTrackAdjustment: Float? = null,
/** @see RawSong.replayGainAlbumAdjustment */ /** @see RawSong.replayGainAlbumAdjustment */
val replayGainAlbumAdjustment: Float?, val replayGainAlbumAdjustment: Float? = null,
/** @see RawSong.musicBrainzId */ /** @see RawSong.musicBrainzId */
var musicBrainzId: String? = null, var musicBrainzId: String? = null,
/** @see RawSong.name */ /** @see RawSong.name */

View file

@ -32,10 +32,8 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicChoicesBinding import org.oxycblt.auxio.databinding.DialogMusicChoicesBinding
import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.list.ClickableListListener
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.PlaylistDecision
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.navigateSafe
@ -76,7 +74,7 @@ class AddToPlaylistDialog :
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
pickerModel.setSongsToAdd(args.songUids) pickerModel.setSongsToAdd(args.songUids)
collect(musicModel.playlistDecision.flow, ::handleDecision) musicModel.playlistDecision.consume()
collectImmediately(pickerModel.currentSongsToAdd, ::updatePendingSongs) collectImmediately(pickerModel.currentSongsToAdd, ::updatePendingSongs)
collectImmediately(pickerModel.playlistAddChoices, ::updatePlaylistChoices) collectImmediately(pickerModel.playlistAddChoices, ::updatePlaylistChoices)
} }
@ -93,26 +91,16 @@ class AddToPlaylistDialog :
} }
override fun onNewPlaylist() { override fun onNewPlaylist() {
musicModel.createPlaylist(songs = pickerModel.currentSongsToAdd.value ?: return) // TODO: This is a temporary fix. Eventually I want to make this navigate away and
} // instead have primary fragments launch navigation to the new playlist dialog.
// This should be better design (dialog layering is uh... probably not good) and
private fun handleDecision(decision: PlaylistDecision?) { // preserves the existing navigation system.
when (decision) { // I could also roll some kind of new playlist textbox into the dialog, but that's
is PlaylistDecision.Add -> { // a lot harder.
logD("Navigated to playlist add dialog") val songs = pickerModel.currentSongsToAdd.value ?: return
musicModel.playlistDecision.consume()
}
is PlaylistDecision.New -> {
logD("Navigating to new playlist dialog")
findNavController() findNavController()
.navigateSafe( .navigateSafe(
AddToPlaylistDialogDirections.newPlaylist( AddToPlaylistDialogDirections.newPlaylist(songs.map { it.uid }.toTypedArray()))
decision.songs.map { it.uid }.toTypedArray()))
}
is PlaylistDecision.Rename,
is PlaylistDecision.Delete -> error("Unexpected decision $decision")
null -> {}
}
} }
private fun updatePendingSongs(songs: List<Song>?) { private fun updatePendingSongs(songs: List<Song>?) {

View file

@ -32,6 +32,8 @@ import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.fs.contentResolverSafe import org.oxycblt.auxio.music.fs.contentResolverSafe
import org.oxycblt.auxio.music.fs.useQuery import org.oxycblt.auxio.music.fs.useQuery
import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.metadata.Separators
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -107,7 +109,7 @@ interface DeviceLibrary {
*/ */
suspend fun create( suspend fun create(
rawSongs: Channel<RawSong>, rawSongs: Channel<RawSong>,
processedSongs: Channel<RawSong> processedSongs: Channel<RawSong>,
): DeviceLibraryImpl ): DeviceLibraryImpl
} }
} }
@ -118,6 +120,9 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu
rawSongs: Channel<RawSong>, rawSongs: Channel<RawSong>,
processedSongs: Channel<RawSong> processedSongs: Channel<RawSong>
): DeviceLibraryImpl { ): DeviceLibraryImpl {
val nameFactory = Name.Known.Factory.from(musicSettings)
val separators = Separators.from(musicSettings)
val songGrouping = mutableMapOf<Music.UID, SongImpl>() val songGrouping = mutableMapOf<Music.UID, SongImpl>()
val albumGrouping = mutableMapOf<RawAlbum.Key, Grouping<RawAlbum, SongImpl>>() val albumGrouping = mutableMapOf<RawAlbum.Key, Grouping<RawAlbum, SongImpl>>()
val artistGrouping = mutableMapOf<RawArtist.Key, Grouping<RawArtist, Music>>() val artistGrouping = mutableMapOf<RawArtist.Key, Grouping<RawArtist, Music>>()
@ -127,7 +132,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu
// All music information is grouped as it is indexed by other components. // All music information is grouped as it is indexed by other components.
for (rawSong in rawSongs) { for (rawSong in rawSongs) {
val song = SongImpl(rawSong, musicSettings) val song = SongImpl(rawSong, nameFactory, separators)
// At times the indexer produces duplicate songs, try to filter these. Comparing by // At times the indexer produces duplicate songs, try to filter these. Comparing by
// UID is sufficient for something like this, and also prevents collisions from // UID is sufficient for something like this, and also prevents collisions from
// causing severe issues elsewhere. // causing severe issues elsewhere.
@ -207,7 +212,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu
// Now that all songs are processed, also process albums and group them into their // Now that all songs are processed, also process albums and group them into their
// respective artists. // respective artists.
val albums = albumGrouping.values.mapTo(mutableSetOf()) { AlbumImpl(it, musicSettings) } val albums = albumGrouping.values.mapTo(mutableSetOf()) { AlbumImpl(it, nameFactory) }
for (album in albums) { for (album in albums) {
for (rawArtist in album.rawArtists) { for (rawArtist in album.rawArtists) {
val key = RawArtist.Key(rawArtist) val key = RawArtist.Key(rawArtist)
@ -243,8 +248,8 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu
} }
// Artists and genres do not need to be grouped and can be processed immediately. // Artists and genres do not need to be grouped and can be processed immediately.
val artists = artistGrouping.values.mapTo(mutableSetOf()) { ArtistImpl(it, musicSettings) } val artists = artistGrouping.values.mapTo(mutableSetOf()) { ArtistImpl(it, nameFactory) }
val genres = genreGrouping.values.mapTo(mutableSetOf()) { GenreImpl(it, musicSettings) } val genres = genreGrouping.values.mapTo(mutableSetOf()) { GenreImpl(it, nameFactory) }
return DeviceLibraryImpl(songGrouping.values.toSet(), albums, artists, genres) return DeviceLibraryImpl(songGrouping.values.toSet(), albums, artists, genres)
} }

View file

@ -25,7 +25,6 @@ import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.fs.MimeType import org.oxycblt.auxio.music.fs.MimeType
@ -36,8 +35,8 @@ import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.Disc
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.music.info.ReleaseType
import org.oxycblt.auxio.music.metadata.Separators
import org.oxycblt.auxio.music.metadata.parseId3GenreNames import org.oxycblt.auxio.music.metadata.parseId3GenreNames
import org.oxycblt.auxio.music.metadata.parseMultiValue
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
import org.oxycblt.auxio.util.positiveOrNull import org.oxycblt.auxio.util.positiveOrNull
import org.oxycblt.auxio.util.toUuidOrNull import org.oxycblt.auxio.util.toUuidOrNull
@ -48,10 +47,15 @@ import org.oxycblt.auxio.util.update
* Library-backed implementation of [Song]. * Library-backed implementation of [Song].
* *
* @param rawSong The [RawSong] to derive the member data from. * @param rawSong The [RawSong] to derive the member data from.
* @param musicSettings [MusicSettings] to for user parsing configuration. * @param nameFactory The [Name.Known.Factory] to interpret name information with.
* @param separators The [Separators] to parse multi-value tags with.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Song { class SongImpl(
private val rawSong: RawSong,
private val nameFactory: Name.Known.Factory,
private val separators: Separators
) : Song {
override val uid = override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID. // Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
rawSong.musicBrainzId?.toUuidOrNull()?.let { Music.UID.musicBrainz(MusicType.SONGS, it) } rawSong.musicBrainzId?.toUuidOrNull()?.let { Music.UID.musicBrainz(MusicType.SONGS, it) }
@ -70,10 +74,8 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
update(rawSong.albumArtistNames) update(rawSong.albumArtistNames)
} }
override val name = override val name =
Name.Known.from( nameFactory.parse(
requireNotNull(rawSong.name) { "Invalid raw: No title" }, requireNotNull(rawSong.name) { "Invalid raw: No title" }, rawSong.sortName)
rawSong.sortName,
musicSettings)
override val track = rawSong.track override val track = rawSong.track
override val disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) } override val disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) }
@ -95,42 +97,11 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
track = rawSong.replayGainTrackAdjustment, album = rawSong.replayGainAlbumAdjustment) track = rawSong.replayGainTrackAdjustment, album = rawSong.replayGainAlbumAdjustment)
override val dateAdded = requireNotNull(rawSong.dateAdded) { "Invalid raw: No date added" } override val dateAdded = requireNotNull(rawSong.dateAdded) { "Invalid raw: No date added" }
private var _album: AlbumImpl? = null private var _album: AlbumImpl? = null
override val album: Album override val album: Album
get() = unlikelyToBeNull(_album) get() = unlikelyToBeNull(_album)
private val hashCode = 31 * uid.hashCode() + rawSong.hashCode()
override fun hashCode() = hashCode
override fun equals(other: Any?) =
other is SongImpl && uid == other.uid && rawSong == other.rawSong
override fun toString() = "Song(uid=$uid, name=$name)"
private val artistMusicBrainzIds = rawSong.artistMusicBrainzIds.parseMultiValue(musicSettings)
private val artistNames = rawSong.artistNames.parseMultiValue(musicSettings)
private val artistSortNames = rawSong.artistSortNames.parseMultiValue(musicSettings)
private val rawIndividualArtists =
artistNames.mapIndexed { i, name ->
RawArtist(
artistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(),
name,
artistSortNames.getOrNull(i))
}
private val albumArtistMusicBrainzIds =
rawSong.albumArtistMusicBrainzIds.parseMultiValue(musicSettings)
private val albumArtistNames = rawSong.albumArtistNames.parseMultiValue(musicSettings)
private val albumArtistSortNames = rawSong.albumArtistSortNames.parseMultiValue(musicSettings)
private val rawAlbumArtists =
albumArtistNames.mapIndexed { i, name ->
RawArtist(
albumArtistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(),
name,
albumArtistSortNames.getOrNull(i))
}
private val _artists = mutableListOf<ArtistImpl>() private val _artists = mutableListOf<ArtistImpl>()
override val artists: List<Artist> override val artists: List<Artist>
get() = _artists get() = _artists
@ -143,41 +114,91 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
* The [RawAlbum] instances collated by the [Song]. This can be used to group [Song]s into an * The [RawAlbum] instances collated by the [Song]. This can be used to group [Song]s into an
* [Album]. * [Album].
*/ */
val rawAlbum = val rawAlbum: RawAlbum
RawAlbum(
mediaStoreId = requireNotNull(rawSong.albumMediaStoreId) { "Invalid raw: No album id" },
musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(),
name = requireNotNull(rawSong.albumName) { "Invalid raw: No album name" },
sortName = rawSong.albumSortName,
releaseType = ReleaseType.parse(rawSong.releaseTypes.parseMultiValue(musicSettings)),
rawArtists =
rawAlbumArtists
.ifEmpty { rawIndividualArtists }
.distinctBy { it.key }
.ifEmpty { listOf(RawArtist(null, null)) })
/** /**
* The [RawArtist] instances collated by the [Song]. The artists of the song take priority, * The [RawArtist] instances collated by the [Song]. The artists of the song take priority,
* followed by the album artists. If there are no artists, this field will be a single "unknown" * followed by the album artists. If there are no artists, this field will be a single "unknown"
* [RawArtist]. This can be used to group up [Song]s into an [Artist]. * [RawArtist]. This can be used to group up [Song]s into an [Artist].
*/ */
val rawArtists = val rawArtists: List<RawArtist>
rawIndividualArtists
.ifEmpty { rawAlbumArtists }
.distinctBy { it.key }
.ifEmpty { listOf(RawArtist()) }
/** /**
* The [RawGenre] instances collated by the [Song]. This can be used to group up [Song]s into a * The [RawGenre] instances collated by the [Song]. This can be used to group up [Song]s into a
* [Genre]. ID3v2 Genre names are automatically converted to their resolved names. * [Genre]. ID3v2 Genre names are automatically converted to their resolved names.
*/ */
val rawGenres = val rawGenres: List<RawGenre>
rawSong.genreNames
.parseId3GenreNames(musicSettings) private var hashCode: Int = uid.hashCode()
.map { RawGenre(it) }
.distinctBy { it.key } init {
val artistMusicBrainzIds = separators.split(rawSong.artistMusicBrainzIds)
val artistNames = separators.split(rawSong.artistNames)
val artistSortNames = separators.split(rawSong.artistSortNames)
val rawIndividualArtists =
artistNames
.mapIndexedTo(mutableSetOf()) { i, name ->
RawArtist(
artistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(),
name,
artistSortNames.getOrNull(i))
}
.toList()
val albumArtistMusicBrainzIds = separators.split(rawSong.albumArtistMusicBrainzIds)
val albumArtistNames = separators.split(rawSong.albumArtistNames)
val albumArtistSortNames = separators.split(rawSong.albumArtistSortNames)
val rawAlbumArtists =
albumArtistNames
.mapIndexedTo(mutableSetOf()) { i, name ->
RawArtist(
albumArtistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(),
name,
albumArtistSortNames.getOrNull(i))
}
.toList()
rawAlbum =
RawAlbum(
mediaStoreId =
requireNotNull(rawSong.albumMediaStoreId) { "Invalid raw: No album id" },
musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(),
name = requireNotNull(rawSong.albumName) { "Invalid raw: No album name" },
sortName = rawSong.albumSortName,
releaseType = ReleaseType.parse(separators.split(rawSong.releaseTypes)),
rawArtists =
rawAlbumArtists
.ifEmpty { rawIndividualArtists }
.ifEmpty { listOf(RawArtist()) })
rawArtists =
rawIndividualArtists.ifEmpty { rawAlbumArtists }.ifEmpty { listOf(RawArtist()) }
val genreNames =
(rawSong.genreNames.parseId3GenreNames() ?: separators.split(rawSong.genreNames))
rawGenres =
genreNames
.mapTo(mutableSetOf()) { RawGenre(it) }
.toList()
.ifEmpty { listOf(RawGenre()) } .ifEmpty { listOf(RawGenre()) }
hashCode = 31 * rawSong.hashCode()
hashCode = 31 * nameFactory.hashCode()
}
override fun hashCode() = hashCode
// Since equality on public-facing music models is not identical to the tag equality,
// we just compare raw instances and how they are interpreted.
override fun equals(other: Any?) =
other is SongImpl &&
uid == other.uid &&
nameFactory == other.nameFactory &&
separators == other.separators &&
rawSong == other.rawSong
override fun toString() = "Song(uid=$uid, name=$name)"
/** /**
* Links this [Song] with a parent [Album]. * Links this [Song] with a parent [Album].
* *
@ -242,12 +263,12 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
* Library-backed implementation of [Album]. * Library-backed implementation of [Album].
* *
* @param grouping [Grouping] to derive the member data from. * @param grouping [Grouping] to derive the member data from.
* @param musicSettings [MusicSettings] to for user parsing configuration. * @param nameFactory The [Name.Known.Factory] to interpret name information with.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class AlbumImpl( class AlbumImpl(
grouping: Grouping<RawAlbum, SongImpl>, grouping: Grouping<RawAlbum, SongImpl>,
musicSettings: MusicSettings, private val nameFactory: Name.Known.Factory
) : Album { ) : Album {
private val rawAlbum = grouping.raw.inner private val rawAlbum = grouping.raw.inner
@ -261,7 +282,7 @@ class AlbumImpl(
update(rawAlbum.name) update(rawAlbum.name)
update(rawAlbum.rawArtists.map { it.name }) update(rawAlbum.rawArtists.map { it.name })
} }
override val name = Name.Known.from(rawAlbum.name, rawAlbum.sortName, musicSettings) override val name = nameFactory.parse(rawAlbum.name, rawAlbum.sortName)
override val dates: Date.Range? override val dates: Date.Range?
override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null) override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null)
override val coverUri = CoverUri(rawAlbum.mediaStoreId.toCoverUri(), grouping.raw.src.uri) override val coverUri = CoverUri(rawAlbum.mediaStoreId.toCoverUri(), grouping.raw.src.uri)
@ -311,13 +332,20 @@ class AlbumImpl(
dateAdded = earliestDateAdded dateAdded = earliestDateAdded
hashCode = 31 * hashCode + rawAlbum.hashCode() hashCode = 31 * hashCode + rawAlbum.hashCode()
hashCode = 31 * nameFactory.hashCode()
hashCode = 31 * hashCode + songs.hashCode() hashCode = 31 * hashCode + songs.hashCode()
} }
override fun hashCode() = hashCode override fun hashCode() = hashCode
// Since equality on public-facing music models is not identical to the tag equality,
// we just compare raw instances and how they are interpreted.
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is AlbumImpl && uid == other.uid && rawAlbum == other.rawAlbum && songs == other.songs other is AlbumImpl &&
uid == other.uid &&
rawAlbum == other.rawAlbum &&
nameFactory == other.nameFactory &&
songs == other.songs
override fun toString() = "Album(uid=$uid, name=$name)" override fun toString() = "Album(uid=$uid, name=$name)"
@ -362,10 +390,13 @@ class AlbumImpl(
* Library-backed implementation of [Artist]. * Library-backed implementation of [Artist].
* *
* @param grouping [Grouping] to derive the member data from. * @param grouping [Grouping] to derive the member data from.
* @param musicSettings [MusicSettings] to for user parsing configuration. * @param nameFactory The [Name.Known.Factory] to interpret name information with.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class ArtistImpl(grouping: Grouping<RawArtist, Music>, musicSettings: MusicSettings) : Artist { class ArtistImpl(
grouping: Grouping<RawArtist, Music>,
private val nameFactory: Name.Known.Factory
) : Artist {
private val rawArtist = grouping.raw.inner private val rawArtist = grouping.raw.inner
override val uid = override val uid =
@ -373,7 +404,7 @@ class ArtistImpl(grouping: Grouping<RawArtist, Music>, musicSettings: MusicSetti
rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicType.ARTISTS, it) } rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicType.ARTISTS, it) }
?: Music.UID.auxio(MusicType.ARTISTS) { update(rawArtist.name) } ?: Music.UID.auxio(MusicType.ARTISTS) { update(rawArtist.name) }
override val name = override val name =
rawArtist.name?.let { Name.Known.from(it, rawArtist.sortName, musicSettings) } rawArtist.name?.let { nameFactory.parse(it, rawArtist.sortName) }
?: Name.Unknown(R.string.def_artist) ?: Name.Unknown(R.string.def_artist)
override val songs: Set<Song> override val songs: Set<Song>
@ -414,6 +445,7 @@ class ArtistImpl(grouping: Grouping<RawArtist, Music>, musicSettings: MusicSetti
durationMs = songs.sumOf { it.durationMs }.positiveOrNull() durationMs = songs.sumOf { it.durationMs }.positiveOrNull()
hashCode = 31 * hashCode + rawArtist.hashCode() hashCode = 31 * hashCode + rawArtist.hashCode()
hashCode = 31 * hashCode + nameFactory.hashCode()
hashCode = 31 * hashCode + songs.hashCode() hashCode = 31 * hashCode + songs.hashCode()
} }
@ -421,10 +453,13 @@ class ArtistImpl(grouping: Grouping<RawArtist, Music>, musicSettings: MusicSetti
// the same UID but different songs are not equal. // the same UID but different songs are not equal.
override fun hashCode() = hashCode override fun hashCode() = hashCode
// Since equality on public-facing music models is not identical to the tag equality,
// we just compare raw instances and how they are interpreted.
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is ArtistImpl && other is ArtistImpl &&
uid == other.uid && uid == other.uid &&
rawArtist == other.rawArtist && rawArtist == other.rawArtist &&
nameFactory == other.nameFactory &&
songs == other.songs songs == other.songs
override fun toString() = "Artist(uid=$uid, name=$name)" override fun toString() = "Artist(uid=$uid, name=$name)"
@ -459,15 +494,18 @@ class ArtistImpl(grouping: Grouping<RawArtist, Music>, musicSettings: MusicSetti
* Library-backed implementation of [Genre]. * Library-backed implementation of [Genre].
* *
* @param grouping [Grouping] to derive the member data from. * @param grouping [Grouping] to derive the member data from.
* @param musicSettings [MusicSettings] to for user parsing configuration. * @param nameFactory The [Name.Known.Factory] to interpret name information with.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class GenreImpl(grouping: Grouping<RawGenre, SongImpl>, musicSettings: MusicSettings) : Genre { class GenreImpl(
grouping: Grouping<RawGenre, SongImpl>,
private val nameFactory: Name.Known.Factory
) : Genre {
private val rawGenre = grouping.raw.inner private val rawGenre = grouping.raw.inner
override val uid = Music.UID.auxio(MusicType.GENRES) { update(rawGenre.name) } override val uid = Music.UID.auxio(MusicType.GENRES) { update(rawGenre.name) }
override val name = override val name =
rawGenre.name?.let { Name.Known.from(it, rawGenre.name, musicSettings) } rawGenre.name?.let { nameFactory.parse(it, rawGenre.name) }
?: Name.Unknown(R.string.def_genre) ?: Name.Unknown(R.string.def_genre)
override val songs: Set<Song> override val songs: Set<Song>
@ -491,13 +529,18 @@ class GenreImpl(grouping: Grouping<RawGenre, SongImpl>, musicSettings: MusicSett
durationMs = totalDuration durationMs = totalDuration
hashCode = 31 * hashCode + rawGenre.hashCode() hashCode = 31 * hashCode + rawGenre.hashCode()
hashCode = 31 * nameFactory.hashCode()
hashCode = 31 * hashCode + songs.hashCode() hashCode = 31 * hashCode + songs.hashCode()
} }
override fun hashCode() = hashCode override fun hashCode() = hashCode
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is GenreImpl && uid == other.uid && rawGenre == other.rawGenre && songs == other.songs other is GenreImpl &&
uid == other.uid &&
rawGenre == other.rawGenre &&
nameFactory == other.nameFactory &&
songs == other.songs
override fun toString() = "Genre(uid=$uid, name=$name)" override fun toString() = "Genre(uid=$uid, name=$name)"

View file

@ -20,6 +20,7 @@ package org.oxycblt.auxio.music.info
import android.content.Context import android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import java.text.CollationKey import java.text.CollationKey
import java.text.Collator import java.text.Collator
import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.MusicSettings
@ -54,36 +55,7 @@ sealed interface Name : Comparable<Name> {
abstract val sort: String? abstract val sort: String?
/** A tokenized version of the name that will be compared. */ /** A tokenized version of the name that will be compared. */
protected abstract val sortTokens: List<SortToken> @VisibleForTesting(VisibleForTesting.PROTECTED) abstract val sortTokens: List<SortToken>
/** An individual part of a name string that can be compared intelligently. */
protected data class SortToken(val collationKey: CollationKey, val type: Type) :
Comparable<SortToken> {
override fun compareTo(other: SortToken): Int {
// Numeric tokens should always be lower than lexicographic tokens.
val modeComp = type.compareTo(other.type)
if (modeComp != 0) {
return modeComp
}
// Numeric strings must be ordered by magnitude, thus immediately short-circuit
// the comparison if the lengths do not match.
if (type == Type.NUMERIC &&
collationKey.sourceString.length != other.collationKey.sourceString.length) {
return collationKey.sourceString.length - other.collationKey.sourceString.length
}
return collationKey.compareTo(other.collationKey)
}
/** Denotes the type of comparison to be performed with this token. */
enum class Type {
/** Compare as a digit string, like "65". */
NUMERIC,
/** Compare as a standard alphanumeric string, like "65daysofstatic" */
LEXICOGRAPHIC
}
}
final override val thumb: String final override val thumb: String
get() = get() =
@ -108,19 +80,29 @@ sealed interface Name : Comparable<Name> {
is Unknown -> 1 is Unknown -> 1
} }
companion object { interface Factory {
/** /**
* Create a new instance of [Name.Known] * Create a new instance of [Name.Known]
* *
* @param raw The raw name obtained from the music item * @param raw The raw name obtained from the music item
* @param sort The raw sort name obtained from the music item * @param sort The raw sort name obtained from the music item
* @param musicSettings [MusicSettings] required for name configuration.
*/ */
fun from(raw: String, sort: String?, musicSettings: MusicSettings): Known = fun parse(raw: String, sort: String?): Known
if (musicSettings.intelligentSorting) {
IntelligentKnownName(raw, sort) companion object {
/**
* Creates a new instance from the **current state** of the given [MusicSettings]'s
* user-defined name configuration.
*
* @param settings The [MusicSettings] to use.
* @return A [Factory] instance reflecting the configuration state.
*/
fun from(settings: MusicSettings) =
if (settings.intelligentSorting) {
IntelligentKnownName.Factory
} else { } else {
SimpleKnownName(raw, sort) SimpleKnownName.Factory
}
} }
} }
} }
@ -148,22 +130,28 @@ sealed interface Name : Comparable<Name> {
private val collator: Collator = Collator.getInstance().apply { strength = Collator.PRIMARY } private val collator: Collator = Collator.getInstance().apply { strength = Collator.PRIMARY }
private val punctRegex by lazy { Regex("[\\p{Punct}+]") } private val punctRegex by lazy { Regex("[\\p{Punct}+]") }
// TODO: Consider how you want to handle whitespace and "gaps" in names.
/** /**
* Plain [Name.Known] implementation that is internationalization-safe. * Plain [Name.Known] implementation that is internationalization-safe.
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
private data class SimpleKnownName(override val raw: String, override val sort: String?) : @VisibleForTesting
Name.Known() { data class SimpleKnownName(override val raw: String, override val sort: String?) : Name.Known() {
override val sortTokens = listOf(parseToken(sort ?: raw)) override val sortTokens = listOf(parseToken(sort ?: raw))
private fun parseToken(name: String): SortToken { private fun parseToken(name: String): SortToken {
// Remove excess punctuation from the string, as those usually aren't considered in sorting. // Remove excess punctuation from the string, as those usually aren't considered in sorting.
val stripped = name.replace(punctRegex, "").ifEmpty { name } val stripped = name.replace(punctRegex, "").trim().ifEmpty { name }
val collationKey = collator.getCollationKey(stripped) val collationKey = collator.getCollationKey(stripped)
// Always use lexicographic mode since we aren't parsing any numeric components // Always use lexicographic mode since we aren't parsing any numeric components
return SortToken(collationKey, SortToken.Type.LEXICOGRAPHIC) return SortToken(collationKey, SortToken.Type.LEXICOGRAPHIC)
} }
data object Factory : Name.Known.Factory {
override fun parse(raw: String, sort: String?) = SimpleKnownName(raw, sort)
}
} }
/** /**
@ -171,7 +159,8 @@ private data class SimpleKnownName(override val raw: String, override val sort:
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
private data class IntelligentKnownName(override val raw: String, override val sort: String?) : @VisibleForTesting
data class IntelligentKnownName(override val raw: String, override val sort: String?) :
Name.Known() { Name.Known() {
override val sortTokens = parseTokens(sort ?: raw) override val sortTokens = parseTokens(sort ?: raw)
@ -180,7 +169,8 @@ private data class IntelligentKnownName(override val raw: String, override val s
// optimize it // optimize it
val stripped = val stripped =
name name
// Remove excess punctuation from the string, as those u // Remove excess punctuation from the string, as those usually aren't
// considered in sorting.
.replace(punctRegex, "") .replace(punctRegex, "")
.ifEmpty { name } .ifEmpty { name }
.run { .run {
@ -218,7 +208,40 @@ private data class IntelligentKnownName(override val raw: String, override val s
} }
} }
data object Factory : Name.Known.Factory {
override fun parse(raw: String, sort: String?) = IntelligentKnownName(raw, sort)
}
companion object { companion object {
private val TOKEN_REGEX by lazy { Regex("(\\d+)|(\\D+)") } private val TOKEN_REGEX by lazy { Regex("(\\d+)|(\\D+)") }
} }
} }
/** An individual part of a name string that can be compared intelligently. */
@VisibleForTesting(VisibleForTesting.PROTECTED)
data class SortToken(val collationKey: CollationKey, val type: Type) : Comparable<SortToken> {
override fun compareTo(other: SortToken): Int {
// Numeric tokens should always be lower than lexicographic tokens.
val modeComp = type.compareTo(other.type)
if (modeComp != 0) {
return modeComp
}
// Numeric strings must be ordered by magnitude, thus immediately short-circuit
// the comparison if the lengths do not match.
if (type == Type.NUMERIC &&
collationKey.sourceString.length != other.collationKey.sourceString.length) {
return collationKey.sourceString.length - other.collationKey.sourceString.length
}
return collationKey.compareTo(other.collationKey)
}
/** Denotes the type of comparison to be performed with this token. */
enum class Type {
/** Compare as a digit string, like "65". */
NUMERIC,
/** Compare as a standard alphanumeric string, like "65daysofstatic" */
LEXICOGRAPHIC
}
}

View file

@ -0,0 +1,76 @@
/*
* Copyright (c) 2023 Auxio Project
* Separators.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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 <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.metadata
import androidx.annotation.VisibleForTesting
import org.oxycblt.auxio.music.MusicSettings
/**
* Defines the user-specified parsing of multi-value tags. This should be used to parse any tags
* that may be delimited with a separator character.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface Separators {
/**
* Parse a separated value from one or more strings. If the value is already composed of more
* than one value, nothing is done. Otherwise, it will attempt to split it based on the user's
* separator preferences.
*
* @return A new list of one or more [String]s parsed by the separator configuration
*/
fun split(strings: List<String>): List<String>
companion object {
const val COMMA = ','
const val SEMICOLON = ';'
const val SLASH = '/'
const val PLUS = '+'
const val AND = '&'
/**
* Creates a new instance from the **current state** of the given [MusicSettings]'s
* user-defined separator configuration.
*
* @param settings The [MusicSettings] to use.
* @return A new [Separators] instance reflecting the configuration state.
*/
fun from(settings: MusicSettings) = from(settings.separators)
@VisibleForTesting
fun from(chars: String) =
if (chars.isNotEmpty()) {
CharSeparators(chars.toSet())
} else {
NoSeparators
}
}
}
private data class CharSeparators(private val chars: Set<Char>) : Separators {
override fun split(strings: List<String>) =
if (strings.size == 1) splitImpl(strings.first()) else strings
private fun splitImpl(string: String) =
string.splitEscaped { chars.contains(it) }.correctWhitespace()
}
private object NoSeparators : Separators {
override fun split(strings: List<String>) = strings
}

View file

@ -52,7 +52,7 @@ class SeparatorsDialog : ViewBindingMaterialDialogFragment<DialogSeparatorsBindi
.setTitle(R.string.set_separators) .setTitle(R.string.set_separators)
.setNegativeButton(R.string.lbl_cancel, null) .setNegativeButton(R.string.lbl_cancel, null)
.setPositiveButton(R.string.lbl_save) { _, _ -> .setPositiveButton(R.string.lbl_save) { _, _ ->
musicSettings.multiValueSeparators = getCurrentSeparators() musicSettings.separators = getCurrentSeparators()
} }
} }
@ -68,8 +68,7 @@ class SeparatorsDialog : ViewBindingMaterialDialogFragment<DialogSeparatorsBindi
// More efficient to do one iteration through the separator list and initialize // More efficient to do one iteration through the separator list and initialize
// the corresponding CheckBox for each character instead of doing an iteration // the corresponding CheckBox for each character instead of doing an iteration
// through the separator list for each CheckBox. // through the separator list for each CheckBox.
(savedInstanceState?.getString(KEY_PENDING_SEPARATORS) (savedInstanceState?.getString(KEY_PENDING_SEPARATORS) ?: musicSettings.separators)
?: musicSettings.multiValueSeparators)
.forEach { .forEach {
when (it) { when (it) {
Separators.COMMA -> binding.separatorComma.isChecked = true Separators.COMMA -> binding.separatorComma.isChecked = true
@ -102,14 +101,6 @@ class SeparatorsDialog : ViewBindingMaterialDialogFragment<DialogSeparatorsBindi
return separators return separators
} }
private object Separators {
const val COMMA = ','
const val SEMICOLON = ';'
const val SLASH = '/'
const val PLUS = '+'
const val AND = '&'
}
private companion object { private companion object {
const val KEY_PENDING_SEPARATORS = BuildConfig.APPLICATION_ID + ".key.PENDING_SEPARATORS" const val KEY_PENDING_SEPARATORS = BuildConfig.APPLICATION_ID + ".key.PENDING_SEPARATORS"
} }

View file

@ -18,29 +18,15 @@
package org.oxycblt.auxio.music.metadata package org.oxycblt.auxio.music.metadata
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.util.positiveOrNull import org.oxycblt.auxio.util.positiveOrNull
/// --- GENERIC PARSING --- /// --- GENERIC PARSING ---
/**
* Parse a multi-value tag based on the user configuration. If the value is already composed of more
* than one value, nothing is done. Otherwise, this function will attempt to split it based on the
* user's separator preferences.
*
* @param settings [MusicSettings] required to obtain user separator configuration.
* @return A new list of one or more [String]s.
*/
fun List<String>.parseMultiValue(settings: MusicSettings) =
if (size == 1) {
first().maybeParseBySeparators(settings)
} else {
// Nothing to do.
this
}
// TODO: Remove the escaping checks, it's too expensive to do this for every single tag. // TODO: Remove the escaping checks, it's too expensive to do this for every single tag.
// TODO: I want to eventually be able to move a lot of this into TagWorker once I no longer have
// to deal with the cross-module dependencies of MediaStoreExtractor.
/** /**
* Split a [String] by the given selector, automatically handling escaped characters that satisfy * Split a [String] by the given selector, automatically handling escaped characters that satisfy
* the selector. * the selector.
@ -101,17 +87,6 @@ fun String.correctWhitespace() = trim().ifBlank { null }
*/ */
fun List<String>.correctWhitespace() = mapNotNull { it.correctWhitespace() } fun List<String>.correctWhitespace() = mapNotNull { it.correctWhitespace() }
/**
* Attempt to parse a string by the user's separator preferences.
*
* @param settings [MusicSettings] required to obtain user separator configuration.
* @return A list of one or more [String]s that were split up by the user-defined separators.
*/
private fun String.maybeParseBySeparators(settings: MusicSettings): List<String> {
if (settings.multiValueSeparators.isEmpty()) return listOf(this)
return splitEscaped { settings.multiValueSeparators.contains(it) }.correctWhitespace()
}
/// --- ID3v2 PARSING --- /// --- ID3v2 PARSING ---
/** /**
@ -165,12 +140,12 @@ fun transformPositionField(pos: Int?, total: Int?) =
* representations of genre fields into their named counterparts, and split up singular ID3v2-style * representations of genre fields into their named counterparts, and split up singular ID3v2-style
* integer genre fields into one or more genres. * integer genre fields into one or more genres.
* *
* @param settings [MusicSettings] required to obtain user separator configuration. * @return A list of one or more genre names, or null if this multi-value list has no valid
* @return A list of one or more genre names.. * formatting.
*/ */
fun List<String>.parseId3GenreNames(settings: MusicSettings) = fun List<String>.parseId3GenreNames() =
if (size == 1) { if (size == 1) {
first().parseId3MultiValueGenre(settings) first().parseId3MultiValueGenre()
} else { } else {
// Nothing to split, just map any ID3v1 genres to their name counterparts. // Nothing to split, just map any ID3v1 genres to their name counterparts.
map { it.parseId3v1Genre() ?: it } map { it.parseId3v1Genre() ?: it }
@ -179,11 +154,10 @@ fun List<String>.parseId3GenreNames(settings: MusicSettings) =
/** /**
* Parse a single ID3v1/ID3v2 integer genre field into their named representations. * Parse a single ID3v1/ID3v2 integer genre field into their named representations.
* *
* @param settings [MusicSettings] required to obtain user separator configuration. * @return list of one or more genre names, or null if this is not in ID3v2 format.
* @return A list of one or more genre names.
*/ */
private fun String.parseId3MultiValueGenre(settings: MusicSettings) = private fun String.parseId3MultiValueGenre() =
parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre() ?: maybeParseBySeparators(settings) parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre()
/** /**
* Parse an ID3v1 integer genre field. * Parse an ID3v1 integer genre field.

View file

@ -77,7 +77,6 @@ private class TagWorkerImpl(
private val rawSong: RawSong, private val rawSong: RawSong,
private val future: Future<TrackGroupArray> private val future: Future<TrackGroupArray>
) : TagWorker { ) : TagWorker {
override fun poll(): RawSong? { override fun poll(): RawSong? {
if (!future.isDone) { if (!future.isDone) {
// Not done yet, nothing to do. // Not done yet, nothing to do.

View file

@ -19,7 +19,6 @@
package org.oxycblt.auxio.music.user package org.oxycblt.auxio.music.user
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
@ -51,10 +50,10 @@ private constructor(
* Clone the data in this instance to a new [PlaylistImpl] with the given [name]. * Clone the data in this instance to a new [PlaylistImpl] with the given [name].
* *
* @param name The new name to use. * @param name The new name to use.
* @param musicSettings [MusicSettings] required for name configuration. * @param nameFactory The [Name.Known.Factory] to interpret name information with.
*/ */
fun edit(name: String, musicSettings: MusicSettings) = fun edit(name: String, nameFactory: Name.Known.Factory) =
PlaylistImpl(uid, Name.Known.from(name, null, musicSettings), songs) PlaylistImpl(uid, nameFactory.parse(name, null), songs)
/** /**
* Clone the data in this instance to a new [PlaylistImpl] with the given [Song]s. * Clone the data in this instance to a new [PlaylistImpl] with the given [Song]s.
@ -76,29 +75,26 @@ private constructor(
* *
* @param name The name of the playlist. * @param name The name of the playlist.
* @param songs The songs to initially populate the playlist with. * @param songs The songs to initially populate the playlist with.
* @param musicSettings [MusicSettings] required for name configuration. * @param nameFactory The [Name.Known.Factory] to interpret name information with.
*/ */
fun from(name: String, songs: List<Song>, musicSettings: MusicSettings) = fun from(name: String, songs: List<Song>, nameFactory: Name.Known.Factory) =
PlaylistImpl( PlaylistImpl(Music.UID.auxio(MusicType.PLAYLISTS), nameFactory.parse(name, null), songs)
Music.UID.auxio(MusicType.PLAYLISTS),
Name.Known.from(name, null, musicSettings),
songs)
/** /**
* Populate a new instance from a read [RawPlaylist]. * Populate a new instance from a read [RawPlaylist].
* *
* @param rawPlaylist The [RawPlaylist] to read from. * @param rawPlaylist The [RawPlaylist] to read from.
* @param deviceLibrary The [DeviceLibrary] to initialize from. * @param deviceLibrary The [DeviceLibrary] to initialize from.
* @param musicSettings [MusicSettings] required for name configuration. * @param nameFactory The [Name.Known.Factory] to interpret name information with.
*/ */
fun fromRaw( fun fromRaw(
rawPlaylist: RawPlaylist, rawPlaylist: RawPlaylist,
deviceLibrary: DeviceLibrary, deviceLibrary: DeviceLibrary,
musicSettings: MusicSettings nameFactory: Name.Known.Factory
) = ) =
PlaylistImpl( PlaylistImpl(
rawPlaylist.playlistInfo.playlistUid, rawPlaylist.playlistInfo.playlistUid,
Name.Known.from(rawPlaylist.playlistInfo.name, null, musicSettings), nameFactory.parse(rawPlaylist.playlistInfo.name, null),
rawPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.songUid) }) rawPlaylist.songs.mapNotNull { deviceLibrary.findSong(it.songUid) })
} }
} }

View file

@ -26,6 +26,7 @@ import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.device.DeviceLibrary
import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
@ -144,7 +145,9 @@ constructor(private val playlistDao: PlaylistDao, private val musicSettings: Mus
UserLibrary.Factory { UserLibrary.Factory {
override suspend fun query() = override suspend fun query() =
try { try {
playlistDao.readRawPlaylists() val rawPlaylists = playlistDao.readRawPlaylists()
logD("Successfully read ${rawPlaylists.size} playlists")
rawPlaylists
} catch (e: Exception) { } catch (e: Exception) {
logE("Unable to read playlists: $e") logE("Unable to read playlists: $e")
listOf() listOf()
@ -154,11 +157,10 @@ constructor(private val playlistDao: PlaylistDao, private val musicSettings: Mus
rawPlaylists: List<RawPlaylist>, rawPlaylists: List<RawPlaylist>,
deviceLibrary: DeviceLibrary deviceLibrary: DeviceLibrary
): MutableUserLibrary { ): MutableUserLibrary {
logD("Successfully read ${rawPlaylists.size} playlists") val nameFactory = Name.Known.Factory.from(musicSettings)
// Convert the database playlist information to actual usable playlists.
val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>() val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>()
for (rawPlaylist in rawPlaylists) { for (rawPlaylist in rawPlaylists) {
val playlistImpl = PlaylistImpl.fromRaw(rawPlaylist, deviceLibrary, musicSettings) val playlistImpl = PlaylistImpl.fromRaw(rawPlaylist, deviceLibrary, nameFactory)
playlistMap[playlistImpl.uid] = playlistImpl playlistMap[playlistImpl.uid] = playlistImpl
} }
return UserLibraryImpl(playlistDao, playlistMap, musicSettings) return UserLibraryImpl(playlistDao, playlistMap, musicSettings)
@ -184,7 +186,7 @@ private class UserLibraryImpl(
override fun findPlaylist(name: String) = playlistMap.values.find { it.name.raw == name } override fun findPlaylist(name: String) = playlistMap.values.find { it.name.raw == name }
override suspend fun createPlaylist(name: String, songs: List<Song>): Playlist? { override suspend fun createPlaylist(name: String, songs: List<Song>): Playlist? {
val playlistImpl = PlaylistImpl.from(name, songs, musicSettings) val playlistImpl = PlaylistImpl.from(name, songs, Name.Known.Factory.from(musicSettings))
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
val rawPlaylist = val rawPlaylist =
RawPlaylist( RawPlaylist(
@ -207,7 +209,9 @@ private class UserLibraryImpl(
val playlistImpl = val playlistImpl =
synchronized(this) { synchronized(this) {
requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" } requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" }
.also { playlistMap[it.uid] = it.edit(name, musicSettings) } .also {
playlistMap[it.uid] = it.edit(name, Name.Known.Factory.from(musicSettings))
}
} }
return try { return try {

View file

@ -38,7 +38,6 @@ import kotlin.math.abs
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.detail.Show
import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.list.ListViewModel
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
@ -48,7 +47,6 @@ import org.oxycblt.auxio.playback.queue.QueueViewModel
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.playback.ui.StyledSeekBar import org.oxycblt.auxio.playback.ui.StyledSeekBar
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.lazyReflectedField import org.oxycblt.auxio.util.lazyReflectedField
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -107,7 +105,7 @@ class PlaybackPanelFragment :
playbackModel.song.value?.let { playbackModel.song.value?.let {
// No playback options are actually available in the menu, so use a junk // No playback options are actually available in the menu, so use a junk
// PlaySong option. // PlaySong option.
listModel.openMenu(R.menu.item_playback_song, it, PlaySong.ByItself) listModel.openMenu(R.menu.playback_song, it, PlaySong.ByItself)
} }
} }
} }
@ -120,6 +118,20 @@ class PlaybackPanelFragment :
val recycler = VP_RECYCLER_FIELD.get(this@apply) as RecyclerView val recycler = VP_RECYCLER_FIELD.get(this@apply) as RecyclerView
recycler.isNestedScrollingEnabled = false recycler.isNestedScrollingEnabled = false
} }
// Set up marquee on song information, alongside click handlers that navigate to each
// respective item.
binding.playbackSong.apply {
isSelected = true
setOnClickListener { navigateToCurrentSong() }
}
binding.playbackArtist.apply {
isSelected = true
setOnClickListener { navigateToCurrentArtist() }
}
binding.playbackAlbum.apply {
isSelected = true
setOnClickListener { navigateToCurrentAlbum() }
}
binding.playbackSeekBar.listener = this binding.playbackSeekBar.listener = this
@ -140,7 +152,6 @@ class PlaybackPanelFragment :
collectImmediately(playbackModel.isShuffled, ::updateShuffled) collectImmediately(playbackModel.isShuffled, ::updateShuffled)
collectImmediately(queueModel.queue, ::updateQueue) collectImmediately(queueModel.queue, ::updateQueue)
collectImmediately(queueModel.index, ::updateQueuePosition) collectImmediately(queueModel.index, ::updateQueuePosition)
collect(detailModel.toShow.flow, ::handleShow)
} }
override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) { override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) {
@ -226,25 +237,8 @@ class PlaybackPanelFragment :
requireBinding().playbackShuffle.isActivated = isShuffled requireBinding().playbackShuffle.isActivated = isShuffled
} }
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 -> {}
}
}
override fun navigateToCurrentSong() { override fun navigateToCurrentSong() {
playbackModel.song.value?.let { playbackModel.song.value?.let(detailModel::showAlbum)
detailModel.showAlbum(it)
playbackModel.openMain()
}
} }
override fun navigateToCurrentArtist() { override fun navigateToCurrentArtist() {

View file

@ -338,8 +338,7 @@ constructor(
song, song,
object : BitmapProvider.Target { object : BitmapProvider.Target {
override fun onCompleted(bitmap: Bitmap?) { override fun onCompleted(bitmap: Bitmap?) {
this@MediaSessionComponent.logD( logD("Bitmap loaded, applying media session and posting notification")
"Bitmap loaded, applying media session and posting notification")
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap) builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap)
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap) builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap)
val metadata = builder.build() val metadata = builder.build()

View file

@ -119,7 +119,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
if (!launchedKeyboard) { if (!launchedKeyboard) {
// Auto-open the keyboard when this view is shown // Auto-open the keyboard when this view is shown
this@SearchFragment.logD("Keyboard is not shown yet") logD("Keyboard is not shown yet")
showKeyboard(this) showKeyboard(this)
launchedKeyboard = true launchedKeyboard = true
} }
@ -184,11 +184,11 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
override fun onOpenMenu(item: Music) { override fun onOpenMenu(item: Music) {
when (item) { when (item) {
is Song -> listModel.openMenu(R.menu.item_song, item, searchModel.playWith) is Song -> listModel.openMenu(R.menu.song, item, searchModel.playWith)
is Album -> listModel.openMenu(R.menu.item_album, item) is Album -> listModel.openMenu(R.menu.album, item)
is Artist -> listModel.openMenu(R.menu.item_parent, item) is Artist -> listModel.openMenu(R.menu.parent, item)
is Genre -> listModel.openMenu(R.menu.item_parent, item) is Genre -> listModel.openMenu(R.menu.parent, item)
is Playlist -> listModel.openMenu(R.menu.item_playlist, item) is Playlist -> listModel.openMenu(R.menu.playlist, item)
} }
} }
@ -261,6 +261,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
is Menu.ForArtist -> SearchFragmentDirections.openArtistMenu(menu.parcel) is Menu.ForArtist -> SearchFragmentDirections.openArtistMenu(menu.parcel)
is Menu.ForGenre -> SearchFragmentDirections.openGenreMenu(menu.parcel) is Menu.ForGenre -> SearchFragmentDirections.openGenreMenu(menu.parcel)
is Menu.ForPlaylist -> SearchFragmentDirections.openPlaylistMenu(menu.parcel) is Menu.ForPlaylist -> SearchFragmentDirections.openPlaylistMenu(menu.parcel)
is Menu.ForSelection -> SearchFragmentDirections.openSelectionMenu(menu.parcel)
} }
findNavController().navigateSafe(directions) findNavController().navigateSafe(directions)
// Keyboard is no longer needed. // Keyboard is no longer needed.

View file

@ -18,13 +18,8 @@
package org.oxycblt.auxio.settings package org.oxycblt.auxio.settings
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.core.net.toUri
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
@ -37,8 +32,7 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.openInBrowser
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
/** /**
@ -69,10 +63,10 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
} }
binding.aboutVersion.text = BuildConfig.VERSION_NAME binding.aboutVersion.text = BuildConfig.VERSION_NAME
binding.aboutCode.setOnClickListener { openLinkInBrowser(LINK_SOURCE) } binding.aboutCode.setOnClickListener { requireContext().openInBrowser(LINK_SOURCE) }
binding.aboutWiki.setOnClickListener { openLinkInBrowser(LINK_WIKI) } binding.aboutWiki.setOnClickListener { requireContext().openInBrowser(LINK_WIKI) }
binding.aboutLicenses.setOnClickListener { openLinkInBrowser(LINK_LICENSES) } binding.aboutLicenses.setOnClickListener { requireContext().openInBrowser(LINK_LICENSES) }
binding.aboutAuthor.setOnClickListener { openLinkInBrowser(LINK_AUTHOR) } binding.aboutAuthor.setOnClickListener { requireContext().openInBrowser(LINK_AUTHOR) }
// VIEWMODEL SETUP // VIEWMODEL SETUP
collectImmediately(musicModel.statistics, ::updateStatistics) collectImmediately(musicModel.statistics, ::updateStatistics)
@ -93,74 +87,6 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
(statistics?.durationMs ?: 0).formatDurationMs(false)) (statistics?.durationMs ?: 0).formatDurationMs(false))
} }
/**
* Open the given URI in a web browser.
*
* @param uri The URL to open.
*/
private fun openLinkInBrowser(uri: String) {
logD("Opening $uri")
val context = requireContext()
val browserIntent =
Intent(Intent.ACTION_VIEW, uri.toUri()).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11 seems to now handle the app chooser situations on its own now
// [along with adding a new permission that breaks the old manual code], so
// we just do a typical activity launch.
logD("Using API 30+ chooser")
try {
context.startActivity(browserIntent)
} catch (e: ActivityNotFoundException) {
// No app installed to open the link
context.showToast(R.string.err_no_app)
}
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
// On older versions of android, opening links from an ACTION_VIEW intent might
// not work in all cases, especially when no default app was set. If that is the
// case, we will try to manually handle these cases before we try to launch the
// browser.
logD("Resolving browser activity for chooser")
val pkgName =
context.packageManager
.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
?.run { activityInfo.packageName }
if (pkgName != null) {
if (pkgName == "android") {
// No default browser [Must open app chooser, may not be supported]
logD("No default browser found")
openAppChooser(browserIntent)
} else logD("Opening browser intent")
try {
browserIntent.setPackage(pkgName)
startActivity(browserIntent)
} catch (e: ActivityNotFoundException) {
// Not a browser but an app chooser
browserIntent.setPackage(null)
openAppChooser(browserIntent)
}
} else {
// No app installed to open the link
context.showToast(R.string.err_no_app)
}
}
}
/**
* Open an app chooser for a given [Intent].
*
* @param intent The [Intent] to show an app chooser for.
*/
private fun openAppChooser(intent: Intent) {
logD("Opening app chooser for ${intent.action}")
val chooserIntent =
Intent(Intent.ACTION_CHOOSER)
.putExtra(Intent.EXTRA_INTENT, intent)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(chooserIntent)
}
private companion object { private companion object {
/** The URL to the source code. */ /** The URL to the source code. */
const val LINK_SOURCE = "https://github.com/OxygenCobalt/Auxio" const val LINK_SOURCE = "https://github.com/OxygenCobalt/Auxio"

View file

@ -0,0 +1,81 @@
/*
* Copyright (c) 2023 Auxio Project
* DialogAwareNavigationListener.kt is part of Auxio.
*
* This 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 <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui
import android.os.Bundle
import androidx.navigation.NavController
import androidx.navigation.NavDestination
/**
* A [NavController.OnDestinationChangedListener] that will call [callback] when moving between
* fragments only (not between dialogs or anything similar).
*
* Note: This only works because of special naming used in Auxio's navigation graphs. Keep this in
* mind when porting to other projects.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class DialogAwareNavigationListener(private val callback: () -> Unit) :
NavController.OnDestinationChangedListener {
private var currentDestination: NavDestination? = null
/**
* Attach this instance to a [NavController]. This should be done in the onStart method of a
* Fragment.
*
* @param navController The [NavController] to add to.
*/
fun attach(navController: NavController) {
currentDestination = null
navController.addOnDestinationChangedListener(this)
}
/**
* Remove this listener from it's [NavController]. This should be done in the onStop method of a
* Fragment.
*
* @param navController The [NavController] to remove from. Should be the same on used in
* [attach].
*/
fun release(navController: NavController) {
currentDestination = null
navController.removeOnDestinationChangedListener(this)
}
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.
val lastDestination = currentDestination
currentDestination = destination
if (lastDestination == null) {
return
}
if (!lastDestination.isDialog() && !destination.isDialog()) {
callback()
}
}
private fun NavDestination.isDialog() = label?.endsWith("dialog") == true
}

View file

@ -26,9 +26,11 @@ import android.view.ViewGroup
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BackportBottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BackportBottomSheetDialog
import com.google.android.material.bottomsheet.BackportBottomSheetDialogFragment
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.oxycblt.auxio.util.getDimenPixels
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -39,10 +41,10 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
abstract class ViewBindingBottomSheetDialogFragment<VB : ViewBinding> : abstract class ViewBindingBottomSheetDialogFragment<VB : ViewBinding> :
BottomSheetDialogFragment() { BackportBottomSheetDialogFragment() {
private var _binding: VB? = null private var _binding: VB? = null
override fun onCreateDialog(savedInstanceState: Bundle?): BottomSheetDialog = override fun onCreateDialog(savedInstanceState: Bundle?): BackportBottomSheetDialog =
TweakedBottomSheetDialog(requireContext(), theme) TweakedBottomSheetDialog(requireContext(), theme)
/** /**
@ -109,19 +111,29 @@ abstract class ViewBindingBottomSheetDialogFragment<VB : ViewBinding> :
private inner class TweakedBottomSheetDialog private inner class TweakedBottomSheetDialog
@JvmOverloads @JvmOverloads
constructor(context: Context, @StyleRes theme: Int = 0) : BottomSheetDialog(context, theme) { constructor(context: Context, @StyleRes theme: Int = 0) :
BackportBottomSheetDialog(context, theme) {
private var avoidUnusableCollapsedState = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Collapsed state is bugged in phone landscape mode and shows only 10% of the dialog. // Automatic peek height calculations are bugged in phone landscape mode and show only
// Just disable it and go directly from expanded -> hidden. // 10% of the dialog. Just disable it in that case and go directly from expanded ->
behavior.skipCollapsed = true // hidden.
val metrics = context.resources.displayMetrics
avoidUnusableCollapsedState =
metrics.heightPixels - metrics.widthPixels <
context.getDimenPixels(
com.google.android.material.R.dimen.design_bottom_sheet_peek_height_min)
behavior.skipCollapsed = avoidUnusableCollapsedState
} }
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
// Manually trigger an expanded transition to make window insets actually apply to if (avoidUnusableCollapsedState) {
// the dialog on the first layout pass. I don't know why this works. // skipCollapsed isn't enough, also need to immediately snap to expanded state.
behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.state = BackportBottomSheetBehavior.STATE_EXPANDED
}
} }
} }
} }

View file

@ -18,7 +18,10 @@
package org.oxycblt.auxio.util package org.oxycblt.auxio.util
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.PointF import android.graphics.PointF
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
@ -28,10 +31,12 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.view.menu.ActionMenuItemView import androidx.appcompat.view.menu.ActionMenuItemView
import androidx.appcompat.widget.ActionMenuView import androidx.appcompat.widget.ActionMenuView
import androidx.appcompat.widget.AppCompatButton import androidx.appcompat.widget.AppCompatButton
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.net.toUri
import androidx.core.view.children import androidx.core.view.children
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
@ -40,6 +45,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
@ -111,7 +117,7 @@ val ViewBinding.context: Context
* Override the behavior of a [MaterialToolbar]'s overflow menu to do something else. This is * Override the behavior of a [MaterialToolbar]'s overflow menu to do something else. This is
* extremely dumb, but required to hook overflow menus to bottom sheet menus. * extremely dumb, but required to hook overflow menus to bottom sheet menus.
*/ */
fun MaterialToolbar.overrideOnOverflowMenuClick(block: (View) -> Unit) { fun Toolbar.overrideOnOverflowMenuClick(block: (View) -> Unit) {
for (toolbarChild in children) { for (toolbarChild in children) {
if (toolbarChild is ActionMenuView) { if (toolbarChild is ActionMenuView) {
for (menuChild in toolbarChild.children) { for (menuChild in toolbarChild.children) {
@ -321,3 +327,65 @@ fun Context.share(songs: Collection<Song>) {
builder.setType(mimeTypes.singleOrNull() ?: "audio/*").startChooser() builder.setType(mimeTypes.singleOrNull() ?: "audio/*").startChooser()
} }
/**
* Open the given URI in a web browser.
*
* @param uri The URL to open.
*/
fun Context.openInBrowser(uri: String) {
fun openAppChooser(intent: Intent) {
logD("Opening app chooser for ${intent.action}")
val chooserIntent =
Intent(Intent.ACTION_CHOOSER)
.putExtra(Intent.EXTRA_INTENT, intent)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(chooserIntent)
}
logD("Opening $uri")
val browserIntent =
Intent(Intent.ACTION_VIEW, uri.toUri()).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11 seems to now handle the app chooser situations on its own now
// [along with adding a new permission that breaks the old manual code], so
// we just do a typical activity launch.
logD("Using API 30+ chooser")
try {
startActivity(browserIntent)
} catch (e: ActivityNotFoundException) {
// No app installed to open the link
showToast(R.string.err_no_app)
}
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
// On older versions of android, opening links from an ACTION_VIEW intent might
// not work in all cases, especially when no default app was set. If that is the
// case, we will try to manually handle these cases before we try to launch the
// browser.
logD("Resolving browser activity for chooser")
val pkgName =
packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)?.run {
activityInfo.packageName
}
if (pkgName != null) {
if (pkgName == "android") {
// No default browser [Must open app chooser, may not be supported]
logD("No default browser found")
openAppChooser(browserIntent)
} else logD("Opening browser intent")
try {
browserIntent.setPackage(pkgName)
startActivity(browserIntent)
} catch (e: ActivityNotFoundException) {
// Not a browser but an app chooser
browserIntent.setPackage(null)
openAppChooser(browserIntent)
}
} else {
// No app installed to open the link
showToast(R.string.err_no_app)
}
}
}

View file

@ -82,8 +82,10 @@ fun lazyReflectedField(clazz: KClass<*>, field: String) = lazy {
* @param clazz The [KClass] to reflect into. * @param clazz The [KClass] to reflect into.
* @param method The name of the method to obtain. * @param method The name of the method to obtain.
*/ */
fun lazyReflectedMethod(clazz: KClass<*>, method: String) = lazy { fun lazyReflectedMethod(clazz: KClass<*>, method: String, vararg params: KClass<*>) = lazy {
clazz.java.getDeclaredMethod(method).also { it.isAccessible = true } clazz.java.getDeclaredMethod(method, *params.map { it.java }.toTypedArray()).also {
it.isAccessible = true
}
} }
/** /**

View file

@ -18,27 +18,24 @@
package org.oxycblt.auxio.util package org.oxycblt.auxio.util
import android.util.Log
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import timber.log.Timber
// Shortcut functions for logging.
// Yes, I know timber exists but this does what I need.
/** /**
* Log an object to the debug channel. Automatically handles tags. * Log an object to the debug channel. Automatically handles tags.
* *
* @param obj The object to log. * @param obj The object to log.
*/ */
fun Any.logD(obj: Any?) = logD("$obj") fun logD(obj: Any?) = logD("$obj")
/** /**
* Log a string message to the debug channel. Automatically handles tags. * Log a string message to the debug channel. Automatically handles tags.
* *
* @param msg The message to log. * @param msg The message to log.
*/ */
fun Any.logD(msg: String) { fun logD(msg: String) {
if (BuildConfig.DEBUG && !copyleftNotice()) { if (BuildConfig.DEBUG && !copyleftNotice()) {
Log.d(autoTag, msg) Timber.d(msg)
} }
} }
@ -47,21 +44,14 @@ fun Any.logD(msg: String) {
* *
* @param msg The message to log. * @param msg The message to log.
*/ */
fun Any.logW(msg: String) = Log.w(autoTag, msg) fun logW(msg: String) = Timber.w(msg)
/** /**
* Log a string message to the error channel. Automatically handles tags. * Log a string message to the error channel. Automatically handles tags.
* *
* @param msg The message to log. * @param msg The message to log.
*/ */
fun Any.logE(msg: String) = Log.e(autoTag, msg) fun logE(msg: String) = Timber.e(msg)
/**
* The LogCat-suitable tag for this string. Consists of the object's name, or "Anonymous Object" if
* the object does not exist.
*/
private val Any.autoTag: String
get() = "Auxio.${this::class.simpleName ?: "Anonymous Object"}"
/** /**
* Please don't plagiarize Auxio! You are free to remove this as long as you continue to keep your * Please don't plagiarize Auxio! You are free to remove this as long as you continue to keep your
@ -71,7 +61,7 @@ private val Any.autoTag: String
private fun copyleftNotice(): Boolean { private fun copyleftNotice(): Boolean {
if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" && if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" &&
BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug") { BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug") {
Log.d( Timber.d(
"Auxio Project", "Auxio Project",
"Friendly reminder: Auxio is licensed under the " + "Friendly reminder: Auxio is licensed under the " +
"GPLv3 and all derivative apps must be made open source!") "GPLv3 and all derivative apps must be made open source!")

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M200,880Q167,880 143.5,856.5Q120,833 120,800L120,240L200,240L200,800Q200,800 200,800Q200,800 200,800L640,800L640,880L200,880ZM360,720Q327,720 303.5,696.5Q280,673 280,640L280,160Q280,127 303.5,103.5Q327,80 360,80L720,80Q753,80 776.5,103.5Q800,127 800,160L800,640Q800,673 776.5,696.5Q753,720 720,720L360,720ZM360,640L720,640Q720,640 720,640Q720,640 720,640L720,160Q720,160 720,160Q720,160 720,160L360,160Q360,160 360,160Q360,160 360,160L360,640Q360,640 360,640Q360,640 360,640ZM360,640Q360,640 360,640Q360,640 360,640L360,160Q360,160 360,160Q360,160 360,160L360,160Q360,160 360,160Q360,160 360,160L360,640Q360,640 360,640Q360,640 360,640L360,640Z"/>
</vector>

View file

@ -29,8 +29,8 @@
android:id="@+id/playback_seek_bar" android:id="@+id/playback_seek_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_small" android:layout_marginStart="@dimen/spacing_tiny"
android:layout_marginEnd="@dimen/spacing_small" android:layout_marginEnd="@dimen/spacing_tiny"
app:layout_constraintBottom_toTopOf="@+id/playback_controls_container" app:layout_constraintBottom_toTopOf="@+id/playback_controls_container"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<View
android:id="@+id/touch_outside"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="false"
android:importantForAccessibility="no"
android:soundEffectsEnabled="false"
tools:ignore="UnusedAttribute"/>
<FrameLayout
android:id="@+id/design_bottom_sheet"
style="?attr/bottomSheetStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|top"
app:layout_behavior="com.google.android.material.bottomsheet.BackportBottomSheetBehavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</FrameLayout>

View file

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_large"
android:paddingStart="@dimen/spacing_large"
tools:context=".MainActivity">
<com.google.android.material.card.MaterialCardView
android:id="@+id/error_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
style="@style/Widget.Material3.CardView.Filled"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none">
<TextView
android:id="@+id/error_stack_trace"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@dimen/spacing_medium"
android:paddingStart="@dimen/spacing_medium"
android:paddingBottom="@dimen/spacing_medium"
android:paddingEnd="@dimen/size_copy_button"
android:breakStrategy="simple"
android:hyphenationFrequency="none"
android:typeface="monospace"
tools:text="Stack trace here" />
</HorizontalScrollView>
</ScrollView>
<com.google.android.material.button.MaterialButton
android:id="@+id/error_copy"
style="@style/Widget.Auxio.Button.Icon.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
app:icon="@drawable/ic_copy_24"
android:layout_margin="@dimen/spacing_small"
app:backgroundTint="?attr/colorPrimaryContainer"
android:src="@drawable/ic_code_24" />
</FrameLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -70,8 +70,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_margin="@dimen/spacing_medium" android:layout_margin="@dimen/spacing_medium"
android:fitsSystemWindows="true" android:visibility="invisible"
android:visibility="invisible"> android:fitsSystemWindows="true">
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:layout_width="match_parent" android:layout_width="match_parent"
@ -90,7 +90,7 @@
android:layout_margin="@dimen/spacing_medium" android:layout_margin="@dimen/spacing_medium"
android:gravity="center" android:gravity="center"
android:textAppearance="@style/TextAppearance.Auxio.BodyLarge" android:textAppearance="@style/TextAppearance.Auxio.BodyLarge"
app:layout_constraintBottom_toTopOf="@+id/home_indexing_action" app:layout_constraintBottom_toTopOf="@+id/home_indexing_actions"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" app:layout_constraintVertical_chainStyle="packed"
tools:text="Status" /> tools:text="Status" />
@ -103,20 +103,40 @@
android:layout_marginEnd="@dimen/spacing_medium" android:layout_marginEnd="@dimen/spacing_medium"
android:indeterminate="true" android:indeterminate="true"
app:indeterminateAnimationType="disjoint" app:indeterminateAnimationType="disjoint"
app:layout_constraintBottom_toBottomOf="@+id/home_indexing_action" app:layout_constraintBottom_toBottomOf="@+id/home_indexing_actions"
app:layout_constraintTop_toTopOf="@+id/home_indexing_action" /> app:layout_constraintTop_toTopOf="@+id/home_indexing_actions" />
<org.oxycblt.auxio.ui.RippleFixMaterialButton <LinearLayout
android:id="@+id/home_indexing_action" android:id="@+id/home_indexing_actions"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginStart="@dimen/spacing_medium" android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium" android:layout_marginEnd="@dimen/spacing_medium"
android:layout_marginBottom="@dimen/spacing_medium" android:layout_marginBottom="@dimen/spacing_medium"
android:text="@string/lbl_retry"
android:visibility="invisible" android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_indexing_status" /> tools:layout_editor_absoluteX="16dp">
<org.oxycblt.auxio.ui.RippleFixMaterialButton
android:id="@+id/home_indexing_try"
style="@style/Widget.Auxio.Button.Primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_small"
android:layout_weight="1"
android:text="@string/lbl_retry" />
<org.oxycblt.auxio.ui.RippleFixMaterialButton
android:id="@+id/home_indexing_more"
style="@style/Widget.Auxio.Button.Secondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_small"
android:layout_weight="1"
android:text="@string/lbl_show_error_info" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -56,6 +56,7 @@
android:id="@+id/queue_handle" android:id="@+id/queue_handle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="@dimen/spacing_medium"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_play"
android:title="@string/lbl_play"
android:icon="@drawable/ic_play_24"
app:showAsAction="never"/>
<item
android:id="@+id/action_shuffle"
android:title="@string/lbl_shuffle"
android:icon="@drawable/ic_shuffle_off_24"
app:showAsAction="never"/>
<item
android:id="@+id/action_play_next"
android:title="@string/lbl_play_next"
android:icon="@drawable/ic_play_next_24" />
<item
android:id="@+id/action_queue_add"
android:title="@string/lbl_queue_add"
android:icon="@drawable/ic_queue_add_24" />
<item
android:id="@+id/action_playlist_add"
android:title="@string/lbl_playlist_add"
android:icon="@drawable/ic_playlist_add_24" />
<item
android:id="@+id/action_share"
android:title="@string/lbl_share"
android:icon="@drawable/ic_share_24" />
</menu>

View file

@ -12,19 +12,7 @@
android:icon="@drawable/ic_playlist_add_24" android:icon="@drawable/ic_playlist_add_24"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
<item <item
android:id="@+id/action_selection_queue_add" android:id="@+id/placeholder"
android:title="@string/lbl_queue_add" android:title=""
app:showAsAction="never" />
<item
android:id="@+id/action_selection_play"
android:title="@string/lbl_play_selected"
app:showAsAction="never"/>
<item
android:id="@+id/action_selection_shuffle"
android:title="@string/lbl_shuffle_selected"
app:showAsAction="never"/>
<item
android:id="@+id/action_selection_share"
android:title="@string/lbl_share"
app:showAsAction="never" /> app:showAsAction="never" />
</menu> </menu>

View file

@ -7,7 +7,7 @@
<fragment <fragment
android:id="@+id/home_fragment" android:id="@+id/home_fragment"
android:name="org.oxycblt.auxio.home.HomeFragment" android:name="org.oxycblt.auxio.home.HomeFragment"
android:label="fragment_home" android:label="home_fragment"
tools:layout="@layout/fragment_home"> tools:layout="@layout/fragment_home">
<action <action
android:id="@+id/search" android:id="@+id/search"
@ -57,6 +57,9 @@
<action <action
android:id="@+id/open_playlist_menu" android:id="@+id/open_playlist_menu"
app:destination="@id/playlist_menu_dialog" /> app:destination="@id/playlist_menu_dialog" />
<action
android:id="@+id/open_selection_menu"
app:destination="@id/selection_menu_dialog" />
<action <action
android:id="@+id/new_playlist" android:id="@+id/new_playlist"
app:destination="@id/new_playlist_dialog" /> app:destination="@id/new_playlist_dialog" />
@ -78,8 +81,21 @@
<action <action
android:id="@+id/play_from_genre" android:id="@+id/play_from_genre"
app:destination="@id/play_from_genre_dialog" /> app:destination="@id/play_from_genre_dialog" />
<action
android:id="@+id/report_error"
app:destination="@id/error_details_dialog" />
</fragment> </fragment>
<dialog
android:id="@+id/error_details_dialog"
android:name="org.oxycblt.auxio.home.ErrorDetailsDialog"
android:label="error_details_dialog"
tools:layout="@layout/dialog_error_details">
<argument
android:name="error"
app:argType="java.lang.Exception" />
</dialog>
<dialog <dialog
android:id="@+id/song_sort_dialog" android:id="@+id/song_sort_dialog"
android:name="org.oxycblt.auxio.home.sort.SongSortDialog" android:name="org.oxycblt.auxio.home.sort.SongSortDialog"
@ -89,25 +105,25 @@
<dialog <dialog
android:id="@+id/album_sort_dialog" android:id="@+id/album_sort_dialog"
android:name="org.oxycblt.auxio.home.sort.AlbumSortDialog" android:name="org.oxycblt.auxio.home.sort.AlbumSortDialog"
android:label="song_sort_dialog" android:label="album_sort_dialog"
tools:layout="@layout/dialog_sort" /> tools:layout="@layout/dialog_sort" />
<dialog <dialog
android:id="@+id/artist_sort_dialog" android:id="@+id/artist_sort_dialog"
android:name="org.oxycblt.auxio.home.sort.ArtistSortDialog" android:name="org.oxycblt.auxio.home.sort.ArtistSortDialog"
android:label="song_sort_dialog" android:label="artist_sort_dialog"
tools:layout="@layout/dialog_sort" /> tools:layout="@layout/dialog_sort" />
<dialog <dialog
android:id="@+id/genre_sort_dialog" android:id="@+id/genre_sort_dialog"
android:name="org.oxycblt.auxio.home.sort.GenreSortDialog" android:name="org.oxycblt.auxio.home.sort.GenreSortDialog"
android:label="song_sort_dialog" android:label="genre_sort_dialog"
tools:layout="@layout/dialog_sort" /> tools:layout="@layout/dialog_sort" />
<dialog <dialog
android:id="@+id/playlist_sort_dialog" android:id="@+id/playlist_sort_dialog"
android:name="org.oxycblt.auxio.home.sort.PlaylistSortDialog" android:name="org.oxycblt.auxio.home.sort.PlaylistSortDialog"
android:label="song_sort_dialog" android:label="playlist_sort_dialog"
tools:layout="@layout/dialog_sort" /> tools:layout="@layout/dialog_sort" />
<dialog <dialog
@ -123,7 +139,7 @@
<fragment <fragment
android:id="@+id/search_fragment" android:id="@+id/search_fragment"
android:name="org.oxycblt.auxio.search.SearchFragment" android:name="org.oxycblt.auxio.search.SearchFragment"
android:label="SearchFragment" android:label="search_fragment"
tools:layout="@layout/fragment_search"> tools:layout="@layout/fragment_search">
<action <action
android:id="@+id/show_song" android:id="@+id/show_song"
@ -152,6 +168,9 @@
<action <action
android:id="@+id/open_genre_menu" android:id="@+id/open_genre_menu"
app:destination="@id/genre_menu_dialog" /> app:destination="@id/genre_menu_dialog" />
<action
android:id="@+id/open_selection_menu"
app:destination="@id/selection_menu_dialog" />
<action <action
android:id="@+id/open_playlist_menu" android:id="@+id/open_playlist_menu"
app:destination="@id/playlist_menu_dialog" /> app:destination="@id/playlist_menu_dialog" />
@ -178,7 +197,7 @@
<fragment <fragment
android:id="@+id/album_detail_fragment" android:id="@+id/album_detail_fragment"
android:name="org.oxycblt.auxio.detail.AlbumDetailFragment" android:name="org.oxycblt.auxio.detail.AlbumDetailFragment"
android:label="AlbumDetailFragment" android:label="album_detail_fragment"
tools:layout="@layout/fragment_detail"> tools:layout="@layout/fragment_detail">
<argument <argument
android:name="albumUid" android:name="albumUid"
@ -204,6 +223,9 @@
<action <action
android:id="@+id/open_album_menu" android:id="@+id/open_album_menu"
app:destination="@id/album_menu_dialog" /> app:destination="@id/album_menu_dialog" />
<action
android:id="@+id/open_selection_menu"
app:destination="@id/selection_menu_dialog" />
<action <action
android:id="@+id/add_to_playlist" android:id="@+id/add_to_playlist"
app:destination="@id/add_to_playlist_dialog" /> app:destination="@id/add_to_playlist_dialog" />
@ -218,13 +240,13 @@
<dialog <dialog
android:id="@+id/album_song_sort_dialog" android:id="@+id/album_song_sort_dialog"
android:name="org.oxycblt.auxio.detail.sort.AlbumSongSortDialog" android:name="org.oxycblt.auxio.detail.sort.AlbumSongSortDialog"
android:label="AlbumSongSortDialog" android:label="album_song_sort_dialog"
tools:layout="@layout/dialog_sort" /> tools:layout="@layout/dialog_sort" />
<fragment <fragment
android:id="@+id/artist_detail_fragment" android:id="@+id/artist_detail_fragment"
android:name="org.oxycblt.auxio.detail.ArtistDetailFragment" android:name="org.oxycblt.auxio.detail.ArtistDetailFragment"
android:label="ArtistDetailFragment" android:label="artist_detail_fragment"
tools:layout="@layout/fragment_detail"> tools:layout="@layout/fragment_detail">
<argument <argument
android:name="artistUid" android:name="artistUid"
@ -241,6 +263,9 @@
<action <action
android:id="@+id/show_artist" android:id="@+id/show_artist"
app:destination="@id/artist_detail_fragment" /> app:destination="@id/artist_detail_fragment" />
<action
android:id="@+id/show_artist_choices"
app:destination="@id/show_artist_choices_dialog" />
<action <action
android:id="@+id/open_song_menu" android:id="@+id/open_song_menu"
app:destination="@id/song_menu_dialog" /> app:destination="@id/song_menu_dialog" />
@ -250,6 +275,9 @@
<action <action
android:id="@+id/open_artist_menu" android:id="@+id/open_artist_menu"
app:destination="@id/artist_menu_dialog" /> app:destination="@id/artist_menu_dialog" />
<action
android:id="@+id/open_selection_menu"
app:destination="@id/selection_menu_dialog" />
<action <action
android:id="@+id/add_to_playlist" android:id="@+id/add_to_playlist"
app:destination="@id/add_to_playlist_dialog" /> app:destination="@id/add_to_playlist_dialog" />
@ -261,13 +289,13 @@
<dialog <dialog
android:id="@+id/artist_song_sort_dialog" android:id="@+id/artist_song_sort_dialog"
android:name="org.oxycblt.auxio.detail.sort.ArtistSongSortDialog" android:name="org.oxycblt.auxio.detail.sort.ArtistSongSortDialog"
android:label="ArtistSongSortDialog" android:label="artist_song_sort_dialog"
tools:layout="@layout/dialog_sort" /> tools:layout="@layout/dialog_sort" />
<fragment <fragment
android:id="@+id/genre_detail_fragment" android:id="@+id/genre_detail_fragment"
android:name="org.oxycblt.auxio.detail.GenreDetailFragment" android:name="org.oxycblt.auxio.detail.GenreDetailFragment"
android:label="GenreDetailFragment" android:label="genre_detail_fragment"
tools:layout="@layout/fragment_detail"> tools:layout="@layout/fragment_detail">
<argument <argument
android:name="genreUid" android:name="genreUid"
@ -296,6 +324,9 @@
<action <action
android:id="@+id/open_genre_menu" android:id="@+id/open_genre_menu"
app:destination="@id/genre_menu_dialog" /> app:destination="@id/genre_menu_dialog" />
<action
android:id="@+id/open_selection_menu"
app:destination="@id/selection_menu_dialog" />
<action <action
android:id="@+id/add_to_playlist" android:id="@+id/add_to_playlist"
app:destination="@id/add_to_playlist_dialog" /> app:destination="@id/add_to_playlist_dialog" />
@ -307,13 +338,13 @@
<dialog <dialog
android:id="@+id/genre_song_sort_dialog" android:id="@+id/genre_song_sort_dialog"
android:name="org.oxycblt.auxio.detail.sort.GenreSongSortDialog" android:name="org.oxycblt.auxio.detail.sort.GenreSongSortDialog"
android:label="GenreSongSortDialog" android:label="genre_song_sort_dialog"
tools:layout="@layout/dialog_sort" /> tools:layout="@layout/dialog_sort" />
<fragment <fragment
android:id="@+id/playlist_detail_fragment" android:id="@+id/playlist_detail_fragment"
android:name="org.oxycblt.auxio.detail.PlaylistDetailFragment" android:name="org.oxycblt.auxio.detail.PlaylistDetailFragment"
android:label="PlaylistDetailFragment" android:label="playlist_detail_fragment"
tools:layout="@layout/fragment_detail"> tools:layout="@layout/fragment_detail">
<argument <argument
android:name="playlistUid" android:name="playlistUid"
@ -339,6 +370,9 @@
<action <action
android:id="@+id/open_playlist_menu" android:id="@+id/open_playlist_menu"
app:destination="@id/playlist_menu_dialog" /> app:destination="@id/playlist_menu_dialog" />
<action
android:id="@+id/open_selection_menu"
app:destination="@id/selection_menu_dialog" />
<action <action
android:id="@+id/rename_playlist" android:id="@+id/rename_playlist"
app:destination="@id/rename_playlist_dialog" /> app:destination="@id/rename_playlist_dialog" />
@ -356,7 +390,7 @@
<dialog <dialog
android:id="@+id/playlist_song_sort_dialog" android:id="@+id/playlist_song_sort_dialog"
android:name="org.oxycblt.auxio.detail.sort.PlaylistSongSortDialog" android:name="org.oxycblt.auxio.detail.sort.PlaylistSongSortDialog"
android:label="PlaylistSongSortDialog" android:label="playlist_song_sort_dialog"
tools:layout="@layout/dialog_sort" /> tools:layout="@layout/dialog_sort" />
<dialog <dialog
@ -481,4 +515,14 @@
android:name="parcel" android:name="parcel"
app:argType="org.oxycblt.auxio.list.menu.Menu$ForPlaylist$Parcel" /> app:argType="org.oxycblt.auxio.list.menu.Menu$ForPlaylist$Parcel" />
</dialog> </dialog>
<dialog
android:id="@+id/selection_menu_dialog"
android:name="org.oxycblt.auxio.list.menu.SelectionMenuDialogFragment"
android:label="selection_menu_dialog"
tools:layout="@layout/dialog_menu">
<argument
android:name="parcel"
app:argType="org.oxycblt.auxio.list.menu.Menu$ForSelection$Parcel" />
</dialog>
</navigation> </navigation>

View file

@ -8,7 +8,7 @@
<fragment <fragment
android:id="@+id/main_fragment" android:id="@+id/main_fragment"
android:name="org.oxycblt.auxio.MainFragment" android:name="org.oxycblt.auxio.MainFragment"
android:label="fragment_main" android:label="main_fragment"
tools:layout="@layout/fragment_main"> tools:layout="@layout/fragment_main">
<action <action
android:id="@+id/preferences" android:id="@+id/preferences"
@ -20,7 +20,7 @@
<fragment <fragment
android:id="@+id/root_preferences_fragment" android:id="@+id/root_preferences_fragment"
android:name="org.oxycblt.auxio.settings.RootPreferenceFragment" android:name="org.oxycblt.auxio.settings.RootPreferenceFragment"
android:label="fragment_settings"> android:label="settings_fragment">
<action <action
android:id="@+id/ui_preferences" android:id="@+id/ui_preferences"
app:destination="@id/ui_preferences_fragment" /> app:destination="@id/ui_preferences_fragment" />
@ -41,7 +41,7 @@
<fragment <fragment
android:id="@+id/ui_preferences_fragment" android:id="@+id/ui_preferences_fragment"
android:name="org.oxycblt.auxio.settings.categories.UIPreferenceFragment" android:name="org.oxycblt.auxio.settings.categories.UIPreferenceFragment"
android:label="fragment_ui_preferences"> android:label="ui_preferences_fragment">
<action <action
android:id="@+id/accent_settings" android:id="@+id/accent_settings"
app:destination="@id/accent_dialog" /> app:destination="@id/accent_dialog" />
@ -50,7 +50,7 @@
<fragment <fragment
android:id="@+id/personalize_preferences_fragment" android:id="@+id/personalize_preferences_fragment"
android:name="org.oxycblt.auxio.settings.categories.PersonalizePreferenceFragment" android:name="org.oxycblt.auxio.settings.categories.PersonalizePreferenceFragment"
android:label="fragment_personalize_preferences"> android:label="personalize_preferences_fragment">
<action <action
android:id="@+id/tab_settings" android:id="@+id/tab_settings"
app:destination="@id/tab_dialog" /> app:destination="@id/tab_dialog" />
@ -59,7 +59,7 @@
<fragment <fragment
android:id="@+id/music_preferences_fragment" android:id="@+id/music_preferences_fragment"
android:name="org.oxycblt.auxio.settings.categories.MusicPreferenceFragment" android:name="org.oxycblt.auxio.settings.categories.MusicPreferenceFragment"
android:label="fragment_personalize_preferences"> android:label="personalize_preferences_fragment">
<action <action
android:id="@+id/separators_settings" android:id="@+id/separators_settings"
app:destination="@id/separators_dialog" /> app:destination="@id/separators_dialog" />
@ -68,7 +68,7 @@
<fragment <fragment
android:id="@+id/audio_preferences_fragment" android:id="@+id/audio_preferences_fragment"
android:name="org.oxycblt.auxio.settings.categories.AudioPreferenceFragment" android:name="org.oxycblt.auxio.settings.categories.AudioPreferenceFragment"
android:label="fragment_personalize_preferences"> android:label="personalize_preferences_fragment">
<action <action
android:id="@+id/pre_amp_settings" android:id="@+id/pre_amp_settings"
app:destination="@id/pre_amp_dialog" /> app:destination="@id/pre_amp_dialog" />

View file

@ -145,8 +145,6 @@
<string name="lbl_size">الحجم</string> <string name="lbl_size">الحجم</string>
<string name="lbl_relative_path">المسار</string> <string name="lbl_relative_path">المسار</string>
<string name="lbl_library_counts">إحصائيات المكتبة</string> <string name="lbl_library_counts">إحصائيات المكتبة</string>
<string name="lbl_shuffle_selected">تشغي الاغاني المحددة بترتيب عشوائي</string>
<string name="lbl_play_selected">تشغيل الموسيقى المحددة</string>
<string name="lbl_bitrate">معدل البت</string> <string name="lbl_bitrate">معدل البت</string>
<string name="lbl_file_name">اسم الملف</string> <string name="lbl_file_name">اسم الملف</string>
<string name="lbl_compilation_live">تجميع مباشر</string> <string name="lbl_compilation_live">تجميع مباشر</string>

View file

@ -14,7 +14,6 @@
<string name="lbl_confirm_delete_playlist">حذف قائمة التشغيل؟</string> <string name="lbl_confirm_delete_playlist">حذف قائمة التشغيل؟</string>
<string name="lbl_search">بحث</string> <string name="lbl_search">بحث</string>
<string name="lbl_filter">تصفية</string> <string name="lbl_filter">تصفية</string>
<string name="lbl_play_selected">تشغيل المختارة</string>
<string name="lbl_play_next">تشغيل التالي</string> <string name="lbl_play_next">تشغيل التالي</string>
<string name="lbl_queue_add">إضافة للطابور</string> <string name="lbl_queue_add">إضافة للطابور</string>
<string name="lbl_playlist_add">إضافة لقائمة التشغيل</string> <string name="lbl_playlist_add">إضافة لقائمة التشغيل</string>
@ -28,7 +27,6 @@
<string name="lbl_new_playlist">قائمة تشغيل جديدة</string> <string name="lbl_new_playlist">قائمة تشغيل جديدة</string>
<string name="lbl_rename_playlist">إعادة تسمية قائمة التشغيل</string> <string name="lbl_rename_playlist">إعادة تسمية قائمة التشغيل</string>
<string name="lbl_edit">تعديل</string> <string name="lbl_edit">تعديل</string>
<string name="lbl_shuffle_selected">خلط المختارة</string>
<string name="lbl_queue">طابور</string> <string name="lbl_queue">طابور</string>
<string name="lbl_shuffle">خلط</string> <string name="lbl_shuffle">خلط</string>
<string name="lbl_artist_details">اذهب للفنان</string> <string name="lbl_artist_details">اذهب للفنان</string>

View file

@ -72,7 +72,6 @@
<string name="lbl_playback">Зараз іграе</string> <string name="lbl_playback">Зараз іграе</string>
<string name="lbl_play">Гуляць</string> <string name="lbl_play">Гуляць</string>
<string name="lbl_shuffle">Ператасаваць</string> <string name="lbl_shuffle">Ператасаваць</string>
<string name="lbl_shuffle_selected">Выбрана перамешванне</string>
<string name="lbl_size">Памер</string> <string name="lbl_size">Памер</string>
<string name="lbl_shuffle_shortcut_short">Ператасаваць</string> <string name="lbl_shuffle_shortcut_short">Ператасаваць</string>
<string name="lbl_cancel">Адмяніць</string> <string name="lbl_cancel">Адмяніць</string>
@ -81,7 +80,6 @@
<string name="lbl_play_next">Гуляць далей</string> <string name="lbl_play_next">Гуляць далей</string>
<string name="lbl_queue_add">Дадаць у чаргу</string> <string name="lbl_queue_add">Дадаць у чаргу</string>
<string name="lbl_equalizer">Эквалайзер</string> <string name="lbl_equalizer">Эквалайзер</string>
<string name="lbl_play_selected">Гуляць выбрана</string>
<string name="lbl_queue">Чарга</string> <string name="lbl_queue">Чарга</string>
<string name="lbl_album_details">Перайсці да альбома</string> <string name="lbl_album_details">Перайсці да альбома</string>
<string name="lbl_artist_details">Перайсці да выканаўцы</string> <string name="lbl_artist_details">Перайсці да выканаўцы</string>
@ -298,4 +296,8 @@
<string name="lbl_song">Песня</string> <string name="lbl_song">Песня</string>
<string name="set_play_song_by_itself">Прайграць песню самастойна</string> <string name="set_play_song_by_itself">Прайграць песню самастойна</string>
<string name="lbl_parent_detail">Выгляд</string> <string name="lbl_parent_detail">Выгляд</string>
<string name="lbl_sort_mode">Сартаваць па</string>
<string name="lbl_sort_direction">Напрамак</string>
<string name="desc_selection_image">Абярыце малюнак</string>
<string name="lbl_selection">Абярыце</string>
</resources> </resources>

View file

@ -260,9 +260,7 @@
<string name="err_did_not_wipe">Nepodařilo se vymazat stav</string> <string name="err_did_not_wipe">Nepodařilo se vymazat stav</string>
<string name="set_rescan">Znovu najít hudbu</string> <string name="set_rescan">Znovu najít hudbu</string>
<string name="set_rescan_desc">Vymazat mezipaměť značek a znovu úplně znovu načíst hudební knihovnu (pomalejší, ale úplnější)</string> <string name="set_rescan_desc">Vymazat mezipaměť značek a znovu úplně znovu načíst hudební knihovnu (pomalejší, ale úplnější)</string>
<string name="lbl_play_selected">Přehrát vybrané</string>
<string name="fmt_selected">Vybráno %d</string> <string name="fmt_selected">Vybráno %d</string>
<string name="lbl_shuffle_selected">Náhodně přehrát vybrané</string>
<string name="set_play_song_from_genre">Přehrát z žánru</string> <string name="set_play_song_from_genre">Přehrát z žánru</string>
<string name="lbl_wiki">Wiki</string> <string name="lbl_wiki">Wiki</string>
<string name="fmt_list">%1$s, %2$s</string> <string name="fmt_list">%1$s, %2$s</string>
@ -309,4 +307,8 @@
<string name="lbl_song">Skladba</string> <string name="lbl_song">Skladba</string>
<string name="lbl_parent_detail">Zobrazit</string> <string name="lbl_parent_detail">Zobrazit</string>
<string name="set_play_song_by_itself">Přehrát skladbu samostatně</string> <string name="set_play_song_by_itself">Přehrát skladbu samostatně</string>
<string name="lbl_sort_direction">Směr</string>
<string name="lbl_sort_mode">Seřadit podle</string>
<string name="desc_selection_image">Výběr obrázku</string>
<string name="lbl_selection">Výběr</string>
</resources> </resources>

View file

@ -251,8 +251,6 @@
<string name="err_did_not_save">Zustand konnte nicht gespeichert werden</string> <string name="err_did_not_save">Zustand konnte nicht gespeichert werden</string>
<string name="set_rescan">Music neu scannen</string> <string name="set_rescan">Music neu scannen</string>
<string name="set_rescan_desc">Tag-Cache leeren und die Musik-Bibliothek vollständig neu laden (langsamer, aber vollständiger)</string> <string name="set_rescan_desc">Tag-Cache leeren und die Musik-Bibliothek vollständig neu laden (langsamer, aber vollständiger)</string>
<string name="lbl_play_selected">Ausgewählte abspielen</string>
<string name="lbl_shuffle_selected">Ausgewählte zufällig abspielen</string>
<string name="fmt_selected">%d ausgewählt</string> <string name="fmt_selected">%d ausgewählt</string>
<string name="set_play_song_from_genre">Vom Genre abspielen</string> <string name="set_play_song_from_genre">Vom Genre abspielen</string>
<string name="lbl_wiki">Wiki</string> <string name="lbl_wiki">Wiki</string>
@ -299,4 +297,7 @@
<string name="set_square_covers_desc">Alle Album-Cover auf ein Seitenverhältnis von 1:1 zuschneiden</string> <string name="set_square_covers_desc">Alle Album-Cover auf ein Seitenverhältnis von 1:1 zuschneiden</string>
<string name="lbl_song">Lied</string> <string name="lbl_song">Lied</string>
<string name="lbl_parent_detail">Ansehen</string> <string name="lbl_parent_detail">Ansehen</string>
<string name="set_play_song_by_itself">Lied selbst spielen</string>
<string name="lbl_sort_direction">Richtung</string>
<string name="lbl_sort_mode">Sortieren nach</string>
</resources> </resources>

View file

@ -135,8 +135,6 @@
<string name="lbl_compilation_live">Σύνθεση ζωντανών κομματιών</string> <string name="lbl_compilation_live">Σύνθεση ζωντανών κομματιών</string>
<string name="lbl_compilation_remix">Σύνθεση ρεμίξ</string> <string name="lbl_compilation_remix">Σύνθεση ρεμίξ</string>
<string name="lbl_equalizer">Ισοσταθμιστής</string> <string name="lbl_equalizer">Ισοσταθμιστής</string>
<string name="lbl_play_selected">Αναπαραγωγή επιλεγμένου</string>
<string name="lbl_shuffle_selected">Τυχαία αναπαραγωγή επιλεγμένων</string>
<string name="lbl_single">Ενιαία κυκλοφορία</string> <string name="lbl_single">Ενιαία κυκλοφορία</string>
<string name="lbl_singles">Σινγκλ</string> <string name="lbl_singles">Σινγκλ</string>
</resources> </resources>

View file

@ -255,9 +255,7 @@
<string name="err_did_not_wipe">No se puede borrar el estado</string> <string name="err_did_not_wipe">No se puede borrar el estado</string>
<string name="set_rescan_desc">Borrar la caché de las etiquetas y recargar completamente la biblioteca musical (más lento, pero más completo)</string> <string name="set_rescan_desc">Borrar la caché de las etiquetas y recargar completamente la biblioteca musical (más lento, pero más completo)</string>
<string name="set_rescan">Volver a escanear la música</string> <string name="set_rescan">Volver a escanear la música</string>
<string name="lbl_shuffle_selected">Nodo aleatorio seleccionado</string>
<string name="fmt_selected">%d seleccionado</string> <string name="fmt_selected">%d seleccionado</string>
<string name="lbl_play_selected">Reproducir los seleccionados</string>
<string name="set_play_song_from_genre">Reproducir desde el género</string> <string name="set_play_song_from_genre">Reproducir desde el género</string>
<string name="lbl_wiki">Wiki</string> <string name="lbl_wiki">Wiki</string>
<string name="fmt_list">%1$s, %2$s</string> <string name="fmt_list">%1$s, %2$s</string>
@ -304,4 +302,8 @@
<string name="lbl_song">Canción</string> <string name="lbl_song">Canción</string>
<string name="lbl_parent_detail">Vista</string> <string name="lbl_parent_detail">Vista</string>
<string name="set_play_song_by_itself">Reproducir la canción por tí mismo</string> <string name="set_play_song_by_itself">Reproducir la canción por tí mismo</string>
<string name="lbl_sort_mode">Ordenar por</string>
<string name="lbl_sort_direction">Dirección</string>
<string name="desc_selection_image">Selección de imágenes</string>
<string name="lbl_selection">Selección</string>
</resources> </resources>

View file

@ -37,7 +37,6 @@
<string name="lbl_playback">Nyt toistetaan</string> <string name="lbl_playback">Nyt toistetaan</string>
<string name="lbl_equalizer">Taajuuskorjain</string> <string name="lbl_equalizer">Taajuuskorjain</string>
<string name="lbl_play">Toista</string> <string name="lbl_play">Toista</string>
<string name="lbl_play_selected">Toisto valittu</string>
<string name="lbl_shuffle">Sekoita</string> <string name="lbl_shuffle">Sekoita</string>
<string name="lbl_queue">Jono</string> <string name="lbl_queue">Jono</string>
<string name="lbl_queue_add">Lisää jonoon</string> <string name="lbl_queue_add">Lisää jonoon</string>
@ -219,7 +218,6 @@
<string name="set_replay_gain">ReplayGain</string> <string name="set_replay_gain">ReplayGain</string>
<string name="set_replay_gain_mode_album">Suosi albumia</string> <string name="set_replay_gain_mode_album">Suosi albumia</string>
<string name="set_replay_gain_mode">ReplayGain-strategia</string> <string name="set_replay_gain_mode">ReplayGain-strategia</string>
<string name="lbl_shuffle_selected">Sekoitus valittu</string>
<string name="set_observing">Automaattinen uudelleenlataus</string> <string name="set_observing">Automaattinen uudelleenlataus</string>
<string name="set_headset_autoplay">Automaattitoisto kuulokkeilla</string> <string name="set_headset_autoplay">Automaattitoisto kuulokkeilla</string>
<string name="set_headset_autoplay_desc">Aloita aina toisto, kun kuulokkeet yhdistetään (ei välttämättä toimi kaikilla laitteilla)</string> <string name="set_headset_autoplay_desc">Aloita aina toisto, kun kuulokkeet yhdistetään (ei välttämättä toimi kaikilla laitteilla)</string>

View file

@ -134,8 +134,6 @@
<string name="def_genre">Genre inconnu</string> <string name="def_genre">Genre inconnu</string>
<string name="clr_dynamic">Dynamique</string> <string name="clr_dynamic">Dynamique</string>
<string name="clr_cyan">Cyan</string> <string name="clr_cyan">Cyan</string>
<string name="lbl_shuffle_selected">Lecture aléatoire sélectionnée</string>
<string name="lbl_play_selected">Réinitialiser</string>
<string name="err_no_dirs">Aucun dossier</string> <string name="err_no_dirs">Aucun dossier</string>
<string name="desc_music_dir_delete">Supprimer le dossier</string> <string name="desc_music_dir_delete">Supprimer le dossier</string>
<string name="def_artist">Artiste inconnu</string> <string name="def_artist">Artiste inconnu</string>

View file

@ -49,7 +49,6 @@
<string name="lbl_play">Reproducir</string> <string name="lbl_play">Reproducir</string>
<string name="lbl_shuffle">Mezcla</string> <string name="lbl_shuffle">Mezcla</string>
<string name="lbl_play_next">Reproducir seguinte</string> <string name="lbl_play_next">Reproducir seguinte</string>
<string name="lbl_play_selected">Reproducir a selección</string>
<string name="lbl_queue">Cola</string> <string name="lbl_queue">Cola</string>
<string name="lbl_queue_add">Engadir á cola</string> <string name="lbl_queue_add">Engadir á cola</string>
<string name="set_exclude_non_music">Excluir o que non é música</string> <string name="set_exclude_non_music">Excluir o que non é música</string>
@ -126,7 +125,6 @@
<string name="lbl_sort_asc">Ascendente</string> <string name="lbl_sort_asc">Ascendente</string>
<string name="lbl_sort_dsc">Descendente</string> <string name="lbl_sort_dsc">Descendente</string>
<string name="lbl_equalizer">Ecualizador</string> <string name="lbl_equalizer">Ecualizador</string>
<string name="lbl_shuffle_selected">Aleatorio seleccionado</string>
<string name="lbl_sample_rate">Frecuencia de mostraxe</string> <string name="lbl_sample_rate">Frecuencia de mostraxe</string>
<string name="lbl_about">Acerca de</string> <string name="lbl_about">Acerca de</string>
<string name="lng_observing">Monitorizando cambios na túa biblioteca…</string> <string name="lng_observing">Monitorizando cambios na túa biblioteca…</string>

View file

@ -100,8 +100,6 @@
<string name="fmt_deletion_info">%s हटाएँ\? इसे पूर्ववत नहीं किया जा सकता।</string> <string name="fmt_deletion_info">%s हटाएँ\? इसे पूर्ववत नहीं किया जा सकता।</string>
<string name="fmt_lib_song_count">लोड किए गए गाने: %d</string> <string name="fmt_lib_song_count">लोड किए गए गाने: %d</string>
<string name="lbl_sort_dsc">अवरोही</string> <string name="lbl_sort_dsc">अवरोही</string>
<string name="lbl_play_selected">चयनित चलाएँ</string>
<string name="lbl_shuffle_selected">फेरबदल का चयन किया गया</string>
<string name="lbl_state_wiped">स्थिति साफ की गई</string> <string name="lbl_state_wiped">स्थिति साफ की गई</string>
<string name="lbl_state_saved">स्थिति सहेजी गई</string> <string name="lbl_state_saved">स्थिति सहेजी गई</string>
<string name="set_lib_tabs_desc">लायब्रेरी टैब की दृश्यता और क्रम बदलें</string> <string name="set_lib_tabs_desc">लायब्रेरी टैब की दृश्यता और क्रम बदलें</string>
@ -299,4 +297,6 @@
<string name="set_intelligent_sorting">बुद्धिमान छंटाई</string> <string name="set_intelligent_sorting">बुद्धिमान छंटाई</string>
<string name="set_intelligent_sorting_desc">संख्याओं या \"the\" जैसे शब्दों से शुरू होने वाले नामों को सही ढंग से क्रमबद्ध करें (अंग्रेजी भाषा के संगीत के साथ सबसे अच्छा काम करता है)</string> <string name="set_intelligent_sorting_desc">संख्याओं या \"the\" जैसे शब्दों से शुरू होने वाले नामों को सही ढंग से क्रमबद्ध करें (अंग्रेजी भाषा के संगीत के साथ सबसे अच्छा काम करता है)</string>
<string name="set_play_song_by_itself">इसी गीत को चलाएं</string> <string name="set_play_song_by_itself">इसी गीत को चलाएं</string>
<string name="lbl_sort_direction">दिशा</string>
<string name="lbl_sort_mode">के अनुसार क्रमबद्ध करें</string>
</resources> </resources>

View file

@ -25,7 +25,7 @@
<string name="lbl_artist">Izvođač</string> <string name="lbl_artist">Izvođač</string>
<string name="lbl_artists">Izvođači</string> <string name="lbl_artists">Izvođači</string>
<string name="lbl_genres">Žanrovi</string> <string name="lbl_genres">Žanrovi</string>
<string name="lbl_sort">Sortiraj</string> <string name="lbl_sort">Razvrstaj</string>
<string name="lbl_name">Naziv</string> <string name="lbl_name">Naziv</string>
<string name="lbl_date">Godina</string> <string name="lbl_date">Godina</string>
<string name="lbl_duration">Trajanje</string> <string name="lbl_duration">Trajanje</string>
@ -178,7 +178,7 @@
<string name="lbl_filter_all">Sve</string> <string name="lbl_filter_all">Sve</string>
<string name="lbl_queue_add">Dodaj u popis pjesama</string> <string name="lbl_queue_add">Dodaj u popis pjesama</string>
<string name="lng_queue_added">Dodano u popis pjesama</string> <string name="lng_queue_added">Dodano u popis pjesama</string>
<string name="lbl_song_detail">Prikaži svojstva</string> <string name="lbl_song_detail">Pogledaj svojstva</string>
<string name="lbl_artist_details">Idi na izvođača</string> <string name="lbl_artist_details">Idi na izvođača</string>
<string name="lbl_album_details">Idi na album</string> <string name="lbl_album_details">Idi na album</string>
<string name="set_keep_shuffle_desc">Ostavi miješanje omogućeno kada se druga pjesma reproducira</string> <string name="set_keep_shuffle_desc">Ostavi miješanje omogućeno kada se druga pjesma reproducira</string>
@ -212,7 +212,7 @@
<string name="desc_queue_bar">Otvori popis pjesama</string> <string name="desc_queue_bar">Otvori popis pjesama</string>
<string name="lbl_genre">Žanr</string> <string name="lbl_genre">Žanr</string>
<string name="set_separators_comma">Zarez (,)</string> <string name="set_separators_comma">Zarez (,)</string>
<string name="set_separators_and">Ampersand (&amp;)</string> <string name="set_separators_and">Znak i (&amp;)</string>
<string name="lbl_compilation_live">Kompilacija uživo</string> <string name="lbl_compilation_live">Kompilacija uživo</string>
<string name="lbl_compilation_remix">Kompilacija remiksa</string> <string name="lbl_compilation_remix">Kompilacija remiksa</string>
<string name="lbl_mixes">DJ kompilacije</string> <string name="lbl_mixes">DJ kompilacije</string>
@ -247,15 +247,13 @@
<string name="set_rescan">Ponovo pretraži glazbu</string> <string name="set_rescan">Ponovo pretraži glazbu</string>
<string name="set_rescan_desc">Izbriši predmemoriju oznaka i ponovo potpuno učitaj glazbenu biblioteku (sporije, ali potpunije)</string> <string name="set_rescan_desc">Izbriši predmemoriju oznaka i ponovo potpuno učitaj glazbenu biblioteku (sporije, ali potpunije)</string>
<string name="fmt_selected">Odabrano: %d</string> <string name="fmt_selected">Odabrano: %d</string>
<string name="lbl_shuffle_selected">Promiješaj odabrane</string>
<string name="lbl_play_selected">Reproduciraj odabrane</string>
<string name="set_play_song_from_genre">Reproduciraj iz žanra</string> <string name="set_play_song_from_genre">Reproduciraj iz žanra</string>
<string name="lbl_wiki">Wiki</string> <string name="lbl_wiki">Wiki</string>
<string name="fmt_list">%1$s, %2$s</string> <string name="fmt_list">%1$s, %2$s</string>
<string name="lbl_reset">Resetiraj</string> <string name="lbl_reset">Resetiraj</string>
<string name="set_replay_gain">ReplayGain izjednačavanje glasnoće</string> <string name="set_replay_gain">ReplayGain izjednačavanje glasnoće</string>
<string name="set_dirs_list">Mape</string> <string name="set_dirs_list">Mape</string>
<string name="lbl_sort_dsc">Silazni</string> <string name="lbl_sort_dsc">Silazno</string>
<string name="set_ui_desc">Promijenite temu i boje aplikacije</string> <string name="set_ui_desc">Promijenite temu i boje aplikacije</string>
<string name="set_personalize_desc">Prilagodite kontrole i ponašanje korisničkog sučelja</string> <string name="set_personalize_desc">Prilagodite kontrole i ponašanje korisničkog sučelja</string>
<string name="set_content_desc">Upravljajte učitavanjem glazbe i slika</string> <string name="set_content_desc">Upravljajte učitavanjem glazbe i slika</string>
@ -292,4 +290,11 @@
<string name="def_disc">Nema diska</string> <string name="def_disc">Nema diska</string>
<string name="set_square_covers">Prisili kvadratične omote albuma</string> <string name="set_square_covers">Prisili kvadratične omote albuma</string>
<string name="set_square_covers_desc">Odreži sve omote albuma na omjer 1:1</string> <string name="set_square_covers_desc">Odreži sve omote albuma na omjer 1:1</string>
<string name="lbl_song">Pjesma</string>
<string name="lbl_parent_detail">Pogledaj</string>
<string name="lbl_sort_mode">Razvrstaj po</string>
<string name="set_play_song_by_itself">Reproduciraj pjesmu zasebno</string>
<string name="lbl_sort_direction">Smjer</string>
<string name="desc_selection_image">Slika odabira</string>
<string name="lbl_selection">Odabir</string>
</resources> </resources>

View file

@ -75,7 +75,6 @@
<string name="lbl_name">Név</string> <string name="lbl_name">Név</string>
<string name="lbl_date">Dátum</string> <string name="lbl_date">Dátum</string>
<string name="lbl_sort_dsc">Csökkenő</string> <string name="lbl_sort_dsc">Csökkenő</string>
<string name="lbl_play_selected">Kiválasztott lejátszása</string>
<string name="lbl_new_playlist">Új lejátszólista</string> <string name="lbl_new_playlist">Új lejátszólista</string>
<string name="def_genre">Ismeretlen műfaj</string> <string name="def_genre">Ismeretlen műfaj</string>
<string name="desc_skip_next">Ugrás a következő dalra</string> <string name="desc_skip_next">Ugrás a következő dalra</string>
@ -142,7 +141,6 @@
<string name="desc_song_handle">Helyezze át ezt a dalt</string> <string name="desc_song_handle">Helyezze át ezt a dalt</string>
<string name="desc_artist_image">%s előadó fotója</string> <string name="desc_artist_image">%s előadó fotója</string>
<string name="fmt_lib_total_duration">Teljes időtartam: %s</string> <string name="fmt_lib_total_duration">Teljes időtartam: %s</string>
<string name="lbl_shuffle_selected">Kiválasztottak keverése</string>
<string name="set_personalize_desc">UI vezérlők és viselkedés testreszabása</string> <string name="set_personalize_desc">UI vezérlők és viselkedés testreszabása</string>
<string name="set_lib_tabs_desc">A könyvtárfülek láthatóságának és sorrendjének módosítása</string> <string name="set_lib_tabs_desc">A könyvtárfülek láthatóságának és sorrendjének módosítása</string>
<string name="set_play_in_parent_with">A tétel részleteiből történő lejátszáskor</string> <string name="set_play_in_parent_with">A tétel részleteiből történő lejátszáskor</string>
@ -299,4 +297,8 @@
<string name="lbl_song">Dal</string> <string name="lbl_song">Dal</string>
<string name="lbl_parent_detail">Megnéz</string> <string name="lbl_parent_detail">Megnéz</string>
<string name="set_play_song_by_itself">Dal lejátszása önmagában</string> <string name="set_play_song_by_itself">Dal lejátszása önmagában</string>
<string name="lbl_sort_direction">Irány</string>
<string name="lbl_sort_mode">Rendezés</string>
<string name="lbl_selection">Kiválasztás</string>
<string name="desc_selection_image">Kép kiválasztás</string>
</resources> </resources>

View file

@ -183,8 +183,6 @@
<string name="set_observing">Muat ulang otomatis</string> <string name="set_observing">Muat ulang otomatis</string>
<string name="set_observing_desc">Selalu muat ulang pustaka musik saat terjadi perubahan (membutuhkan notifikasi tetap)</string> <string name="set_observing_desc">Selalu muat ulang pustaka musik saat terjadi perubahan (membutuhkan notifikasi tetap)</string>
<string name="set_behavior">Perilaku</string> <string name="set_behavior">Perilaku</string>
<string name="lbl_play_selected">Putar yang dipilih</string>
<string name="lbl_shuffle_selected">Acak yang dipilih</string>
<string name="set_round_mode">Mode bundar</string> <string name="set_round_mode">Mode bundar</string>
<string name="set_round_mode_desc">Aktifkan sudut yang bundar pada elemen UI tambahan (mewajibkan sampul album bersudut bundar)</string> <string name="set_round_mode_desc">Aktifkan sudut yang bundar pada elemen UI tambahan (mewajibkan sampul album bersudut bundar)</string>
<string name="set_separators_comma">Koma (,)</string> <string name="set_separators_comma">Koma (,)</string>

View file

@ -255,8 +255,6 @@
<string name="err_did_not_save">Impossibile salvare</string> <string name="err_did_not_save">Impossibile salvare</string>
<string name="set_rescan_desc">Svuota la cache dei tag e ricarica completamente la libreria musicale (più lento, ma più completo)</string> <string name="set_rescan_desc">Svuota la cache dei tag e ricarica completamente la libreria musicale (più lento, ma più completo)</string>
<string name="err_did_not_wipe">Impossibile svuotare</string> <string name="err_did_not_wipe">Impossibile svuotare</string>
<string name="lbl_shuffle_selected">Mescola selezionati</string>
<string name="lbl_play_selected">Riproduci selezionati</string>
<string name="fmt_selected">%d selezionati</string> <string name="fmt_selected">%d selezionati</string>
<string name="set_play_song_from_genre">Riproduci dal genere</string> <string name="set_play_song_from_genre">Riproduci dal genere</string>
<string name="lbl_wiki">Wiki</string> <string name="lbl_wiki">Wiki</string>
@ -304,4 +302,5 @@
<string name="lbl_song">Brano</string> <string name="lbl_song">Brano</string>
<string name="lbl_parent_detail">Visualizza</string> <string name="lbl_parent_detail">Visualizza</string>
<string name="set_play_song_by_itself">Riproduci brano da solo</string> <string name="set_play_song_by_itself">Riproduci brano da solo</string>
<string name="lbl_sort_mode">Ordina per</string>
</resources> </resources>

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="lbl_indexer">מוזיקה בטעינה</string> <string name="lbl_indexer">מוזיקה נטענת</string>
<string name="lbl_indexing">מוזיקה בטעינה</string> <string name="lbl_indexing">מוזיקה נטענת</string>
<string name="lbl_retry">לנסות שוב</string> <string name="lbl_retry">לנסות שוב</string>
<string name="lbl_observing">מתבצעת סריקה בספריית המוזיקה שלך</string> <string name="lbl_observing">ספריית המוזיקה שלך נסרקת</string>
<string name="lbl_all_songs">כל השירים</string> <string name="lbl_all_songs">כל השירים</string>
<string name="lbl_albums">אלבומים</string> <string name="lbl_albums">אלבומים</string>
<string name="lbl_album_live">אלבום חי</string> <string name="lbl_album_live">אלבום חי</string>
@ -17,17 +17,17 @@
<string name="lbl_single_live">סינגל חי</string> <string name="lbl_single_live">סינגל חי</string>
<string name="lbl_compilation">אוסף</string> <string name="lbl_compilation">אוסף</string>
<string name="lbl_compilation_live">אוסף חי</string> <string name="lbl_compilation_live">אוסף חי</string>
<string name="lbl_compilation_remix">אוספי רמיקסים</string> <string name="lbl_compilation_remix">אוסף רמיקסים</string>
<string name="lbl_soundtracks">פסקולים</string> <string name="lbl_soundtracks">פסקולים</string>
<string name="lbl_soundtrack">פסקול</string> <string name="lbl_soundtrack">פסקול</string>
<string name="lbl_mixtapes">מיקסטייפים</string> <string name="lbl_mixtapes">מיקסטייפים</string>
<string name="lbl_mix">מיקס</string> <string name="lbl_mix">מיקס DJ</string>
<string name="lbl_live_group">חי</string> <string name="lbl_live_group">חי</string>
<string name="lbl_remix_group">רמיקסים</string> <string name="lbl_remix_group">רמיקסים</string>
<string name="lbl_artist">אומן</string> <string name="lbl_artist">אומן</string>
<string name="lbl_artists">אומנים</string> <string name="lbl_artists">אומנים</string>
<string name="lbl_genre">סוגה</string> <string name="lbl_genre">ז\'אנר</string>
<string name="lbl_genres">סוגות</string> <string name="lbl_genres">ז\'אנרים</string>
<string name="lbl_filter">סינון</string> <string name="lbl_filter">סינון</string>
<string name="lbl_filter_all">הכל</string> <string name="lbl_filter_all">הכל</string>
<string name="lbl_date">תאריך</string> <string name="lbl_date">תאריך</string>
@ -40,15 +40,13 @@
<string name="lbl_playback">מושמע כעת</string> <string name="lbl_playback">מושמע כעת</string>
<string name="lbl_equalizer">איקוולייזר</string> <string name="lbl_equalizer">איקוולייזר</string>
<string name="lbl_play">ניגון</string> <string name="lbl_play">ניגון</string>
<string name="lbl_play_selected">ניגון הנבחרים</string>
<string name="lbl_shuffle">ערבוב</string> <string name="lbl_shuffle">ערבוב</string>
<string name="lbl_shuffle_selected">ערבוב הנבחרים</string>
<string name="lbl_play_next">ניגון הבא</string> <string name="lbl_play_next">ניגון הבא</string>
<string name="lbl_queue_add">הוספה לתור</string> <string name="lbl_queue_add">הוספה לתור</string>
<string name="lbl_album_details">מעבר לאלבום</string> <string name="lbl_album_details">מעבר לאלבום</string>
<string name="lbl_song_detail">הצגת מאפיינים</string> <string name="lbl_song_detail">הצגת מאפיינים</string>
<string name="lbl_props">מאפייני שיר</string> <string name="lbl_props">מאפייני שיר</string>
<string name="lbl_format">תבנית</string> <string name="lbl_format">פורמט</string>
<string name="lbl_size">גודל</string> <string name="lbl_size">גודל</string>
<string name="lbl_bitrate">קצב סיביות</string> <string name="lbl_bitrate">קצב סיביות</string>
<string name="lbl_sample_rate">קצב דגימה</string> <string name="lbl_sample_rate">קצב דגימה</string>
@ -62,11 +60,11 @@
<string name="lbl_version">גרסה</string> <string name="lbl_version">גרסה</string>
<string name="lbl_code">קוד מקור</string> <string name="lbl_code">קוד מקור</string>
<string name="lbl_wiki">ויקי</string> <string name="lbl_wiki">ויקי</string>
<string name="lbl_licenses">רישיונות</string> <string name="lbl_licenses">רשיונות</string>
<string name="lbl_library_counts">סטטיסטיקות ספרייה</string> <string name="lbl_library_counts">סטטיסטיקות ספרייה</string>
<string name="lng_widget">צפייה ושליטה בהשמעת המוזיקה</string> <string name="lng_widget">צפייה ושליטה בהשמעת המוזיקה</string>
<string name="lng_indexing">טוען את ספריית המוזיקה שלך…</string> <string name="lng_indexing">ספריית המוזיקה שלך נטענת</string>
<string name="lng_observing">סורק את ספריית המוזיקה שלך כדי לאתר שינויים…</string> <string name="lng_observing">ספריית המוזיקה שלך נסרקת לאיתור שינויים…</string>
<string name="lng_queue_added">התווסף לתור</string> <string name="lng_queue_added">התווסף לתור</string>
<string name="lng_author">מפותח על ידי אלכסנדר קייפהארט</string> <string name="lng_author">מפותח על ידי אלכסנדר קייפהארט</string>
<string name="lng_search_library">חיפוש בספרייה שלך…</string> <string name="lng_search_library">חיפוש בספרייה שלך…</string>
@ -80,7 +78,7 @@
<string name="set_black_mode_desc">שימוש בערכת נושא שחורה לגמרי</string> <string name="set_black_mode_desc">שימוש בערכת נושא שחורה לגמרי</string>
<string name="set_round_mode">מצב מעוגל</string> <string name="set_round_mode">מצב מעוגל</string>
<string name="set_personalize">התאמה אישית</string> <string name="set_personalize">התאמה אישית</string>
<string name="set_personalize_desc">התאמת רכיבים והתנהגות ממשק המשתמש</string> <string name="set_personalize_desc">התאמת רכיבי והתנהגות הממשק</string>
<string name="set_display">תצוגה</string> <string name="set_display">תצוגה</string>
<string name="set_lib_tabs">לשוניות ספרייה</string> <string name="set_lib_tabs">לשוניות ספרייה</string>
<string name="set_notif_action">פעולת התראות מותאמת אישית</string> <string name="set_notif_action">פעולת התראות מותאמת אישית</string>
@ -93,19 +91,19 @@
<string name="set_play_song_from_all">ניגון מכל השירים</string> <string name="set_play_song_from_all">ניגון מכל השירים</string>
<string name="set_play_song_from_album">ניגון מאלבום</string> <string name="set_play_song_from_album">ניגון מאלבום</string>
<string name="set_play_song_from_artist">ניגון מהאומן</string> <string name="set_play_song_from_artist">ניגון מהאומן</string>
<string name="set_play_song_from_genre">ניגון מסוגה</string> <string name="set_play_song_from_genre">ניגון מז\'אנר</string>
<string name="set_keep_shuffle">לזכור ערבוב</string> <string name="set_keep_shuffle">זכירת ערבוב</string>
<string name="set_keep_shuffle_desc">המשך ערבוב בעת הפעלת שיר חדש</string> <string name="set_keep_shuffle_desc">המשך ערבוב בעת הפעלת שיר חדש</string>
<string name="set_content">תוכן</string> <string name="set_content">תוכן</string>
<string name="set_observing">טעינה מחדש אוטומטית</string> <string name="set_observing">טעינה מחדש אוטומטית</string>
<string name="set_observing_desc">לטעון מחדש את הספרייה בכל פעם שהיא משתנה (דורש התראה קבועה)</string> <string name="set_observing_desc">טעינת הספרייה מחדש בכל פעם שהיא משתנה (דורש התראה קבועה)</string>
<string name="set_exclude_non_music_desc">התעלמות מקובצי שמע שאינם מוזיקה, כמו הסכתים</string> <string name="set_exclude_non_music_desc">התעלמות מקבצי אודיו שאינם מוזיקה, כמו הסכתים</string>
<string name="set_separators">מפרידים רבי-ערכים</string> <string name="set_separators">מפרידים רבי-ערכים</string>
<string name="set_separators_comma">פסיק (,)</string> <string name="set_separators_comma">פסיק (,)</string>
<string name="set_separators_semicolon">נקודה-פסיק (;)</string> <string name="set_separators_semicolon">נקודה-פסיק (;)</string>
<string name="set_separators_plus">פלוס (+)</string> <string name="set_separators_plus">פלוס (+)</string>
<string name="set_separators_and">גם (&amp;)</string> <string name="set_separators_and">גם (&amp;)</string>
<string name="set_hide_collaborators">הסתרת שיתופי פעולה</string> <string name="set_hide_collaborators">הסתרת משתפי~ות פעולה</string>
<string name="set_hide_collaborators_desc">הצגת אומנים שמצויינים ישירות בקרדיטים של אלבום בלבד (עובד באופן מיטבי על ספריות מתויגות היטב)</string> <string name="set_hide_collaborators_desc">הצגת אומנים שמצויינים ישירות בקרדיטים של אלבום בלבד (עובד באופן מיטבי על ספריות מתויגות היטב)</string>
<string name="set_cover_mode">עטיפות אלבום</string> <string name="set_cover_mode">עטיפות אלבום</string>
<string name="set_cover_mode_off">כבוי</string> <string name="set_cover_mode_off">כבוי</string>
@ -118,7 +116,7 @@
<string name="set_repeat_pause">עצירה בעת חזרה</string> <string name="set_repeat_pause">עצירה בעת חזרה</string>
<string name="set_replay_gain">ReplayGain</string> <string name="set_replay_gain">ReplayGain</string>
<string name="set_replay_gain_mode_album">העדפת אלבום</string> <string name="set_replay_gain_mode_album">העדפת אלבום</string>
<string name="set_pre_amp">מגבר עוצמת נגינה מחדש</string> <string name="set_pre_amp">מגבר ReplayGain</string>
<string name="set_pre_amp_with">התאמה עם תגיות</string> <string name="set_pre_amp_with">התאמה עם תגיות</string>
<string name="lbl_mixtape">מיקסטייפ</string> <string name="lbl_mixtape">מיקסטייפ</string>
<string name="info_app_desc">נגן מוזיקה פשוט והגיוני לאנדרואיד.</string> <string name="info_app_desc">נגן מוזיקה פשוט והגיוני לאנדרואיד.</string>
@ -136,24 +134,24 @@
<string name="lbl_file_name">שם קובץ</string> <string name="lbl_file_name">שם קובץ</string>
<string name="lbl_shuffle_shortcut_short">ערבוב</string> <string name="lbl_shuffle_shortcut_short">ערבוב</string>
<string name="lbl_state_restored">המצב שוחזר</string> <string name="lbl_state_restored">המצב שוחזר</string>
<string name="lbl_about">על אודות</string> <string name="lbl_about">אודות</string>
<string name="set_root_title">הגדרות</string> <string name="set_root_title">הגדרות</string>
<string name="set_theme_auto">אוטומטי</string> <string name="set_theme_auto">אוטומטי</string>
<string name="set_round_mode_desc">הפעלת פינות מעוגלות ברכיבי ממשק נוספים (עטיפות אלבומים נדרשות להיות מעוגלות)</string> <string name="set_round_mode_desc">הפעלת פינות מעוגלות ברכיבי ממשק נוספים (עטיפות אלבומים נדרשות להיות מעוגלות)</string>
<string name="set_lib_tabs_desc">שינוי מראה וסדר לשוניות הספרייה</string> <string name="set_lib_tabs_desc">שינוי מראה וסדר לשוניות הספרייה</string>
<string name="set_bar_action">פעולת סרגל השמעה מותאמת אישית</string> <string name="set_bar_action">פעולת סרגל השמעה מותאמת אישית</string>
<string name="set_content_desc">הגדרת טעינת המוזיקה והתמונות</string> <string name="set_content_desc">הגדרת אופן טעינת מוזיקה ותמונות</string>
<string name="set_music">מוזיקה</string> <string name="set_music">מוזיקה</string>
<string name="set_exclude_non_music">אי-הכללת תוכן שאינו מוזיקה</string> <string name="set_exclude_non_music">אי-הכללת תוכן שאינו מוזיקה</string>
<string name="set_separators_desc">התאמת תווים המציינים ערכי תגית מרובים</string> <string name="set_separators_desc">התאמת תווים המציינים ערכי תגית מרובים</string>
<string name="set_separators_slash">קו נטוי (/)</string> <string name="set_separators_slash">קו נטוי (/)</string>
<string name="set_separators_warning">אזהרה: השימוש בהגדרה זו עלול לגרום לחלק מהתגיות להיות מפורשות באופן שגוי כבעלות מספר ערכים. ניתן לפתור זאת על ידי הכנסת קו נטוי אחורי (\\) לפני תווים מפרידים לא רצויים.</string> <string name="set_separators_warning">אזהרה: השימוש בהגדרה זו עלול לגרום לחלק מהתגיות להיות מפורשות באופן שגוי כבעלות מספר ערכים. ניתן לפתור זאת על ידי הכנסת קו נטוי אחורי (\\) לפני תווים מפרידים לא רצויים.</string>
<string name="set_cover_mode_quality">איכות גבוהה</string> <string name="set_cover_mode_quality">איכות גבוהה</string>
<string name="set_intelligent_sorting_desc">התעלמות ממילים כמו \"The\" (\"ה׳ היידוע\") בעת סידור על פי שם (עובד באופן מיטבי עם מוזיקה בשפה האנגלית)</string> <string name="set_intelligent_sorting_desc">התעלמות ממספרים או מילים כמו \"The\" (\"ה׳ היידוע\") בעת סידור על פי שם (עובד באופן מיטבי עם מוזיקה בשפה האנגלית)</string>
<string name="set_images">תמונות</string> <string name="set_images">תמונות</string>
<string name="set_audio_desc">הגדרת הצליל והניגון</string> <string name="set_audio_desc">הגדרת הצליל והניגון</string>
<string name="set_headset_autoplay_desc">תמיד להתחיל לנגן ברגע שמחוברות אזניות (עלול לא לעבוד בכל המערכות)</string> <string name="set_headset_autoplay_desc">תמיד להתחיל לנגן ברגע שמחוברות אזניות (עלול לא לעבוד בכל המערכות)</string>
<string name="set_repeat_pause_desc">השהיה עם חזרה על שיר</string> <string name="set_repeat_pause_desc">השהייה עם חזרה על שיר</string>
<string name="set_replay_gain_mode_track">העדפת רצועה</string> <string name="set_replay_gain_mode_track">העדפת רצועה</string>
<string name="set_replay_gain_mode">אסטרטגיית ReplayGain</string> <string name="set_replay_gain_mode">אסטרטגיית ReplayGain</string>
<string name="set_replay_gain_mode_dynamic">העדפת אלבום אם אחד מופעל</string> <string name="set_replay_gain_mode_dynamic">העדפת אלבום אם אחד מופעל</string>
@ -162,7 +160,7 @@
<string name="lbl_new_playlist">רשימת השמעה חדשה</string> <string name="lbl_new_playlist">רשימת השמעה חדשה</string>
<string name="lbl_playlist_add">הוספה לרשימת השמעה</string> <string name="lbl_playlist_add">הוספה לרשימת השמעה</string>
<string name="lbl_grant">לתת</string> <string name="lbl_grant">לתת</string>
<string name="lbl_playlist">רשימת השמעה</string> <string name="lbl_playlist">רשימת השמעה (פלייליסט)</string>
<string name="lbl_playlists">רשימות השמעה</string> <string name="lbl_playlists">רשימות השמעה</string>
<string name="lbl_delete">מחיקה</string> <string name="lbl_delete">מחיקה</string>
<string name="lbl_rename">שינוי שם</string> <string name="lbl_rename">שינוי שם</string>
@ -172,8 +170,8 @@
<string name="err_did_not_wipe">לא ניתן לנקות את המצב</string> <string name="err_did_not_wipe">לא ניתן לנקות את המצב</string>
<string name="clr_orange">כתום</string> <string name="clr_orange">כתום</string>
<string name="set_dirs">תיקיות מוזיקה</string> <string name="set_dirs">תיקיות מוזיקה</string>
<string name="set_reindex_desc">טעינה מחדש של ספריית המוזיקה, במידה וניתן יעשה שימוש במטמון תגיות</string> <string name="set_reindex_desc">טעינה מחדש של ספריית המוזיקה, במידה וניתן ייעשה שימוש בתגיות מהמטמון</string>
<string name="set_rescan">סריקה מחדש אחר מוזיקה</string> <string name="set_rescan">סריקת מוסיקה מחדש</string>
<string name="set_save_state">שמירת מצב הנגינה</string> <string name="set_save_state">שמירת מצב הנגינה</string>
<string name="err_did_not_save">לא ניתן לשמור את המצב</string> <string name="err_did_not_save">לא ניתן לשמור את המצב</string>
<string name="err_no_perms"> Auxio צריך הרשאות על מנת לקרוא את ספריית המוזיקה שלך</string> <string name="err_no_perms"> Auxio צריך הרשאות על מנת לקרוא את ספריית המוזיקה שלך</string>
@ -185,7 +183,7 @@
<string name="fmt_lib_album_count">אלבומים טעונים: %d</string> <string name="fmt_lib_album_count">אלבומים טעונים: %d</string>
<string name="fmt_lib_genre_count">סוגות טעונות: %d</string> <string name="fmt_lib_genre_count">סוגות טעונות: %d</string>
<string name="lbl_state_wiped">המצב נוקה</string> <string name="lbl_state_wiped">המצב נוקה</string>
<string name="set_library">ספרייה</string> <string name="set_library">ספריה</string>
<string name="set_save_desc">שמירת מצב הנגינה הנוכחי כעת</string> <string name="set_save_desc">שמירת מצב הנגינה הנוכחי כעת</string>
<string name="err_no_app">לא נמצא יישום שיכול לטפל במשימה זו</string> <string name="err_no_app">לא נמצא יישום שיכול לטפל במשימה זו</string>
<string name="err_no_dirs">אין תיקיות</string> <string name="err_no_dirs">אין תיקיות</string>
@ -209,7 +207,7 @@
<string name="clr_dynamic">דינמי</string> <string name="clr_dynamic">דינמי</string>
<string name="fmt_indexing">המוזיקה שלך בטעינה (%1$d/%2$d)…</string> <string name="fmt_indexing">המוזיקה שלך בטעינה (%1$d/%2$d)…</string>
<string name="fmt_disc_no">דיסק %d</string> <string name="fmt_disc_no">דיסק %d</string>
<string name="set_dirs_desc">ניהול תיקיות המוזיקה לטעינה</string> <string name="set_dirs_desc">ניהול המקומות שמהם תיטען מוזיקה</string>
<string name="def_song_count">אין שירים</string> <string name="def_song_count">אין שירים</string>
<string name="clr_pink">ורוד</string> <string name="clr_pink">ורוד</string>
<string name="lng_playlist_created">נוצרה רשימת השמעה</string> <string name="lng_playlist_created">נוצרה רשימת השמעה</string>
@ -235,7 +233,7 @@
<item quantity="two">שני אלבומים</item> <item quantity="two">שני אלבומים</item>
<item quantity="many">%d אלבומים</item> <item quantity="many">%d אלבומים</item>
</plurals> </plurals>
<string name="lng_playlist_renamed">שונה שם לרשימת השמעה</string> <string name="lng_playlist_renamed">שונה שם רשימת ההשמעה</string>
<string name="lng_playlist_deleted">רשימת השמעה נמחקה</string> <string name="lng_playlist_deleted">רשימת השמעה נמחקה</string>
<string name="lng_playlist_added">נוסף לרשימת השמעה</string> <string name="lng_playlist_added">נוסף לרשימת השמעה</string>
<string name="desc_shuffle_all">ערבוב כל השירים</string> <string name="desc_shuffle_all">ערבוב כל השירים</string>
@ -244,7 +242,7 @@
<string name="desc_playlist_image">תמונת רשימת השמעה עבור %s</string> <string name="desc_playlist_image">תמונת רשימת השמעה עבור %s</string>
<string name="clr_red">אדום</string> <string name="clr_red">אדום</string>
<string name="clr_green">ירוק</string> <string name="clr_green">ירוק</string>
<string name="lbl_relative_path">ניתוב הורה</string> <string name="lbl_relative_path">נתיב הורה</string>
<string name="err_did_not_restore">לא ניתן לשחזר את המצב</string> <string name="err_did_not_restore">לא ניתן לשחזר את המצב</string>
<string name="desc_track_number">רצועה %d</string> <string name="desc_track_number">רצועה %d</string>
<string name="desc_new_playlist">יצירת רשימת השמעה חדשה</string> <string name="desc_new_playlist">יצירת רשימת השמעה חדשה</string>
@ -260,4 +258,19 @@
<string name="clr_deep_green">ירוק עמוק</string> <string name="clr_deep_green">ירוק עמוק</string>
<string name="clr_yellow">צהוב</string> <string name="clr_yellow">צהוב</string>
<string name="fmt_deletion_info">מחיקת %s\? פעולה זו לא ניתן לביטול.</string> <string name="fmt_deletion_info">מחיקת %s\? פעולה זו לא ניתן לביטול.</string>
<string name="lbl_song">שיר</string>
<string name="set_intelligent_sorting">מיון חכם</string>
<string name="lbl_parent_detail">הצגה</string>
<string name="set_square_covers">הכרחת עטיפות אלבום מרובעות</string>
<string name="set_rescan_desc">ריקון מטמון התגיות וטעינת ספריית המוזיקה מחדש במלואה (איטי יותר, אך יותר שלם)</string>
<string name="set_wipe_desc">ניקוי מצב הנגינה הקודם שנשמר (אם קיים)</string>
<string name="lbl_sort_mode">מיון על פי</string>
<string name="lbl_sort_direction">כיוון</string>
<string name="set_square_covers_desc">חיתוך כל עטיפות האלבומים ליחס של 1:1</string>
<string name="set_dirs_mode_exclude_desc">מוזיקה <b>לא</b> תיטען מהתיקיות שנוספו.</string>
<string name="set_dirs_mode_include_desc">מוזיקה תיטען <b>רק</b> מהתיקיות שנוספו.</string>
<string name="lbl_appears_on">מופיע~ה ב-</string>
<string name="set_play_song_by_itself">ניגון השיר בלבד</string>
<string name="set_pre_amp_warning">אזהרה: שינוי המגבר לערך חיובי גבוה עלול לגרום לשיאים בחלק מרצועות האודיו</string>
<string name="set_restore_state">שחזור מצב נגינה</string>
</resources> </resources>

View file

@ -63,7 +63,6 @@
<string name="lbl_sort_dsc">降順</string> <string name="lbl_sort_dsc">降順</string>
<string name="lbl_play">再生</string> <string name="lbl_play">再生</string>
<string name="lbl_shuffle">シャフル</string> <string name="lbl_shuffle">シャフル</string>
<string name="lbl_shuffle_selected">選択曲をシャフル</string>
<string name="lbl_play_next">次に再生</string> <string name="lbl_play_next">次に再生</string>
<string name="lbl_queue_add">再生待ちに追加</string> <string name="lbl_queue_add">再生待ちに追加</string>
<string name="lbl_format">オーディオ形式</string> <string name="lbl_format">オーディオ形式</string>
@ -178,7 +177,6 @@
<string name="lbl_ep_remix">リミックスEP</string> <string name="lbl_ep_remix">リミックスEP</string>
<string name="lbl_remix_group">リミックス</string> <string name="lbl_remix_group">リミックス</string>
<string name="lbl_genre">ジャンル</string> <string name="lbl_genre">ジャンル</string>
<string name="lbl_play_selected">選択曲を再生</string>
<string name="lbl_song_detail">プロパティを見る</string> <string name="lbl_song_detail">プロパティを見る</string>
<string name="lbl_queue">再生待ち</string> <string name="lbl_queue">再生待ち</string>
<string name="lbl_library_counts">ライブラリ統計</string> <string name="lbl_library_counts">ライブラリ統計</string>

View file

@ -251,8 +251,6 @@
<item quantity="other">%d 아티스트</item> <item quantity="other">%d 아티스트</item>
</plurals> </plurals>
<string name="set_rescan_desc">태그 정보를 지우고 음악 라이브러리를 재생성함(느림, 더 완전함)</string> <string name="set_rescan_desc">태그 정보를 지우고 음악 라이브러리를 재생성함(느림, 더 완전함)</string>
<string name="lbl_play_selected">선택한 재생</string>
<string name="lbl_shuffle_selected">선택한 셔플</string>
<string name="fmt_selected">%d 선택됨</string> <string name="fmt_selected">%d 선택됨</string>
<string name="lbl_reset">재설정</string> <string name="lbl_reset">재설정</string>
<string name="lbl_wiki">위키</string> <string name="lbl_wiki">위키</string>
@ -295,4 +293,13 @@
<string name="def_disc">디스크 없음</string> <string name="def_disc">디스크 없음</string>
<string name="lng_playlist_deleted">재생목록이 삭제되었습니다</string> <string name="lng_playlist_deleted">재생목록이 삭제되었습니다</string>
<string name="fmt_editing">%s 수정 중</string> <string name="fmt_editing">%s 수정 중</string>
<string name="lbl_song">노래</string>
<string name="lbl_parent_detail">보다</string>
<string name="set_square_covers">포스 스퀘어 앨범 커버</string>
<string name="set_square_covers_desc">모든 앨범 표지를 1:1 가로세로 비율로 자르기</string>
<string name="set_play_song_by_itself">노래 따로 재생</string>
<string name="lbl_sort_direction">방향</string>
<string name="lbl_sort_mode">정렬 기준</string>
<string name="desc_selection_image">선택 이미지</string>
<string name="lbl_selection">선택</string>
</resources> </resources>

View file

@ -249,8 +249,6 @@
<string name="set_rescan">Perskenuoti muziką</string> <string name="set_rescan">Perskenuoti muziką</string>
<string name="set_rescan_desc">Išvalyti žymių talpyklą ir pilnai perkrauti muzikos biblioteką (lėčiau, bet labiau išbaigta)</string> <string name="set_rescan_desc">Išvalyti žymių talpyklą ir pilnai perkrauti muzikos biblioteką (lėčiau, bet labiau išbaigta)</string>
<string name="fmt_selected">%d pasirinkta</string> <string name="fmt_selected">%d pasirinkta</string>
<string name="lbl_play_selected">Pasirinktas grojimas</string>
<string name="lbl_shuffle_selected">Pasirinktas maišymas</string>
<string name="set_play_song_from_genre">Groti iš žanro</string> <string name="set_play_song_from_genre">Groti iš žanro</string>
<string name="lbl_wiki">Viki</string> <string name="lbl_wiki">Viki</string>
<string name="fmt_list">%1$s, %2$s</string> <string name="fmt_list">%1$s, %2$s</string>
@ -297,4 +295,6 @@
<string name="set_square_covers_desc">Apkarpyti visus albumų viršelius iki 1:1 kraštinių koeficiento</string> <string name="set_square_covers_desc">Apkarpyti visus albumų viršelius iki 1:1 kraštinių koeficiento</string>
<string name="set_square_covers">Priversti kvadratinių albumų viršelius</string> <string name="set_square_covers">Priversti kvadratinių albumų viršelius</string>
<string name="set_play_song_by_itself">Groti dainą pačią</string> <string name="set_play_song_by_itself">Groti dainą pačią</string>
<string name="lbl_sort_mode">Rūšiuoti pagal</string>
<string name="lbl_sort_direction">Kryptis</string>
</resources> </resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="lbl_play_selected">തിരഞ്ഞെടുത്തു കളിക്കുക</string>
<string name="lbl_save">രക്ഷിക്കുക</string> <string name="lbl_save">രക്ഷിക്കുക</string>
<string name="set_behavior">പെരുമാറ്റം</string> <string name="set_behavior">പെരുമാറ്റം</string>
<string name="set_content">ഉള്ളടക്കം</string> <string name="set_content">ഉള്ളടക്കം</string>

View file

@ -43,7 +43,6 @@
<string name="lbl_song_count">Sporantall</string> <string name="lbl_song_count">Sporantall</string>
<string name="lbl_queue"></string> <string name="lbl_queue"></string>
<string name="lbl_play_next">Spill neste</string> <string name="lbl_play_next">Spill neste</string>
<string name="lbl_shuffle_selected">Omstokking valgt</string>
<string name="set_library">Bibliotek</string> <string name="set_library">Bibliotek</string>
<string name="err_did_not_save">Kunne ikke lagre tilstand</string> <string name="err_did_not_save">Kunne ikke lagre tilstand</string>
<plurals name="fmt_artist_count"> <plurals name="fmt_artist_count">
@ -275,7 +274,6 @@
<string name="lbl_equalizer">Tonekontroll</string> <string name="lbl_equalizer">Tonekontroll</string>
<string name="desc_change_repeat">Endre gjentagelsesmodus</string> <string name="desc_change_repeat">Endre gjentagelsesmodus</string>
<string name="lbl_play">Spill</string> <string name="lbl_play">Spill</string>
<string name="lbl_play_selected">Spill valgte</string>
<string name="lbl_save">Lagre</string> <string name="lbl_save">Lagre</string>
<string name="lng_indexing">Laster inn musikkbiblioteket ditt …</string> <string name="lng_indexing">Laster inn musikkbiblioteket ditt …</string>
<string name="set_play_in_list_with">Ved avspilling fra bibliotek</string> <string name="set_play_in_list_with">Ved avspilling fra bibliotek</string>

View file

@ -209,7 +209,6 @@
<string name="set_hide_collaborators_desc">Toon alleen artiesten die rechtstreeks op een album worden genoemd (werkt het beste op goed getagde bibliotheken)</string> <string name="set_hide_collaborators_desc">Toon alleen artiesten die rechtstreeks op een album worden genoemd (werkt het beste op goed getagde bibliotheken)</string>
<string name="set_intelligent_sorting_desc">Sorteer namen die beginnen met cijfers of woorden zoals \"de\" correct (werkt het beste met Engelstalige muziek)</string> <string name="set_intelligent_sorting_desc">Sorteer namen die beginnen met cijfers of woorden zoals \"de\" correct (werkt het beste met Engelstalige muziek)</string>
<string name="desc_exit">Stop met afspelen</string> <string name="desc_exit">Stop met afspelen</string>
<string name="lbl_play_selected">Geselecteerd afspelen</string>
<string name="lng_indexing">Uw muziekbibliotheek wordt geladen…</string> <string name="lng_indexing">Uw muziekbibliotheek wordt geladen…</string>
<string name="set_behavior">Gedrag</string> <string name="set_behavior">Gedrag</string>
<string name="lbl_compilation_remix">Remix compilatie</string> <string name="lbl_compilation_remix">Remix compilatie</string>
@ -279,7 +278,6 @@
<item quantity="one">%d artiest</item> <item quantity="one">%d artiest</item>
<item quantity="other">%d artiesten</item> <item quantity="other">%d artiesten</item>
</plurals> </plurals>
<string name="lbl_shuffle_selected">Shuffle geselecteerd</string>
<string name="set_intelligent_sorting">Intelligent sorteren</string> <string name="set_intelligent_sorting">Intelligent sorteren</string>
<string name="lbl_appears_on">Verschijnt op</string> <string name="lbl_appears_on">Verschijnt op</string>
<string name="lbl_playlists">Afspeellijsten</string> <string name="lbl_playlists">Afspeellijsten</string>

View file

@ -49,7 +49,6 @@
<string name="lbl_equalizer">ਇਕੋਲਾਈਜ਼ਰ</string> <string name="lbl_equalizer">ਇਕੋਲਾਈਜ਼ਰ</string>
<string name="lbl_play">ਚਲਾਓ</string> <string name="lbl_play">ਚਲਾਓ</string>
<string name="lbl_shuffle">ਸ਼ਫਲ</string> <string name="lbl_shuffle">ਸ਼ਫਲ</string>
<string name="lbl_shuffle_selected">ਸ਼ਫਲ ਚੁਣਿਆ ਗਿਆ</string>
<string name="lbl_queue">ਕਤਾਰ</string> <string name="lbl_queue">ਕਤਾਰ</string>
<string name="lbl_play_next">ਅਗਲਾ ਚਲਾਓ</string> <string name="lbl_play_next">ਅਗਲਾ ਚਲਾਓ</string>
<string name="lbl_queue_add">ਕਤਾਰ ਵਿੱਚ ਸ਼ਾਮਿਲ ਕਰੋ</string> <string name="lbl_queue_add">ਕਤਾਰ ਵਿੱਚ ਸ਼ਾਮਿਲ ਕਰੋ</string>
@ -76,7 +75,6 @@
<string name="lbl_search">ਖੋਜੋ</string> <string name="lbl_search">ਖੋਜੋ</string>
<string name="lbl_song_count">ਗੀਤ ਦੀ ਗਿਣਤੀ</string> <string name="lbl_song_count">ਗੀਤ ਦੀ ਗਿਣਤੀ</string>
<string name="lbl_sort_dsc">ਘਟਦੇ ਹੋਏ</string> <string name="lbl_sort_dsc">ਘਟਦੇ ਹੋਏ</string>
<string name="lbl_play_selected">ਚੁਣਿਆ ਹੋਇਆ ਚਲਾਓ</string>
<string name="lbl_artist_details">ਕਲਾਕਾਰ \'ਤੇ ਜਾਓ</string> <string name="lbl_artist_details">ਕਲਾਕਾਰ \'ਤੇ ਜਾਓ</string>
<string name="lbl_file_name">ਫਾਈਲ ਦਾ ਨਾਮ</string> <string name="lbl_file_name">ਫਾਈਲ ਦਾ ਨਾਮ</string>
<string name="lbl_bitrate">ਬਿੱਟ ਰੇਟ</string> <string name="lbl_bitrate">ਬਿੱਟ ਰੇਟ</string>
@ -292,4 +290,6 @@
<string name="lbl_song">ਗੀਤ</string> <string name="lbl_song">ਗੀਤ</string>
<string name="lbl_parent_detail">ਵੇਖੋ</string> <string name="lbl_parent_detail">ਵੇਖੋ</string>
<string name="set_play_song_by_itself">ਇਸੇ ਗੀਤ ਨੂੰ ਚਲਾਓ</string> <string name="set_play_song_by_itself">ਇਸੇ ਗੀਤ ਨੂੰ ਚਲਾਓ</string>
<string name="lbl_sort_mode">ਸੌਰਟ ਕਰੋ</string>
<string name="lbl_sort_direction">ਦਿਸ਼ਾ</string>
</resources> </resources>

View file

@ -255,8 +255,6 @@
<string name="set_state">Stan odtwarzania</string> <string name="set_state">Stan odtwarzania</string>
<string name="set_images">Obrazy</string> <string name="set_images">Obrazy</string>
<string name="set_audio_desc">Zarządzaj dźwiękiem i odtwarzaniem muzyki</string> <string name="set_audio_desc">Zarządzaj dźwiękiem i odtwarzaniem muzyki</string>
<string name="lbl_play_selected">Odtwórz wybrane</string>
<string name="lbl_shuffle_selected">Wybrane losowo</string>
<string name="fmt_selected">Wybrano %d</string> <string name="fmt_selected">Wybrano %d</string>
<string name="set_replay_gain">Wyrównanie głośności (ReplayGain)</string> <string name="set_replay_gain">Wyrównanie głośności (ReplayGain)</string>
<string name="lbl_reset">Resetuj</string> <string name="lbl_reset">Resetuj</string>
@ -305,4 +303,6 @@
<string name="lbl_song">Piosenka</string> <string name="lbl_song">Piosenka</string>
<string name="set_play_song_by_itself">Odtwarzanie utworu samodzielnie</string> <string name="set_play_song_by_itself">Odtwarzanie utworu samodzielnie</string>
<string name="lbl_parent_detail">Widok</string> <string name="lbl_parent_detail">Widok</string>
<string name="lbl_sort_mode">Sortuj według</string>
<string name="lbl_sort_direction">Kierunek</string>
</resources> </resources>

View file

@ -253,8 +253,6 @@
<string name="err_did_not_save">Não foi possível salvar a lista</string> <string name="err_did_not_save">Não foi possível salvar a lista</string>
<string name="set_hide_collaborators">Ocultar artistas colaboradores</string> <string name="set_hide_collaborators">Ocultar artistas colaboradores</string>
<string name="set_hide_collaborators_desc">Mostrar apenas artistas que foram creditados diretamente no álbum (funciona melhor em músicas com metadados completos)</string> <string name="set_hide_collaborators_desc">Mostrar apenas artistas que foram creditados diretamente no álbum (funciona melhor em músicas com metadados completos)</string>
<string name="lbl_play_selected">Tocar selecionada(s)</string>
<string name="lbl_shuffle_selected">Aleatorizar selecionadas</string>
<string name="fmt_selected">%d Selecionadas</string> <string name="fmt_selected">%d Selecionadas</string>
<string name="lbl_wiki">Wiki</string> <string name="lbl_wiki">Wiki</string>
<string name="lbl_reset">Redefinir</string> <string name="lbl_reset">Redefinir</string>

View file

@ -217,8 +217,6 @@
<string name="fmt_indexing">A carregar a sua biblioteca de músicas… (%1$d/%2$d)</string> <string name="fmt_indexing">A carregar a sua biblioteca de músicas… (%1$d/%2$d)</string>
<string name="set_rewind_prev">Retroceder antes de voltar</string> <string name="set_rewind_prev">Retroceder antes de voltar</string>
<string name="desc_exit">Parar reprodução</string> <string name="desc_exit">Parar reprodução</string>
<string name="lbl_play_selected">Reproduzir selecionada(s)</string>
<string name="lbl_shuffle_selected">Aleatorizar selecionadas</string>
<string name="lbl_relative_path">Caminho principal</string> <string name="lbl_relative_path">Caminho principal</string>
<string name="set_round_mode_desc">Ativar cantos arredondados em elementos adicionais da interface do utilizador (requer que as capas dos álbuns sejam arredondadas)</string> <string name="set_round_mode_desc">Ativar cantos arredondados em elementos adicionais da interface do utilizador (requer que as capas dos álbuns sejam arredondadas)</string>
<string name="fmt_selected">%d Selecionadas</string> <string name="fmt_selected">%d Selecionadas</string>

View file

@ -134,11 +134,9 @@
<string name="set_display">Afişa</string> <string name="set_display">Afişa</string>
<string name="set_black_mode_desc">Utilizați o temă întunecată pur-negru</string> <string name="set_black_mode_desc">Utilizați o temă întunecată pur-negru</string>
<string name="set_round_mode">Coperți rotunjite ale albumelor</string> <string name="set_round_mode">Coperți rotunjite ale albumelor</string>
<string name="lbl_play_selected">Redare selecție</string>
<string name="lbl_playlist">Listă de redare</string> <string name="lbl_playlist">Listă de redare</string>
<string name="lbl_playlists">Liste de redare</string> <string name="lbl_playlists">Liste de redare</string>
<string name="lbl_sort_dsc">Descrescător</string> <string name="lbl_sort_dsc">Descrescător</string>
<string name="lbl_shuffle_selected">Selecție aleatorie aleasă</string>
<string name="set_action_mode_next">Treceți la următoarea</string> <string name="set_action_mode_next">Treceți la următoarea</string>
<string name="set_play_song_from_artist">Redă de la artist</string> <string name="set_play_song_from_artist">Redă de la artist</string>
<string name="set_play_song_from_genre">Redă din genul</string> <string name="set_play_song_from_genre">Redă din genul</string>

View file

@ -148,15 +148,15 @@
<string name="lbl_ok">ОК</string> <string name="lbl_ok">ОК</string>
<string name="set_play_in_parent_with">При воспроизведении из сведений</string> <string name="set_play_in_parent_with">При воспроизведении из сведений</string>
<string name="set_play_song_none">Воспроизведение с показанного элемента</string> <string name="set_play_song_none">Воспроизведение с показанного элемента</string>
<string name="lbl_song_count">Номер песни</string> <string name="lbl_song_count">Номер трека</string>
<string name="lbl_bitrate">Битрейт</string> <string name="lbl_bitrate">Битрейт</string>
<string name="lbl_disc">Диск</string> <string name="lbl_disc">Диск</string>
<string name="lbl_track">Трек</string> <string name="lbl_track">Трек</string>
<string name="lbl_state_restored">Позиция восстановлена</string> <string name="lbl_state_restored">Позиция восстановлена</string>
<string name="lbl_cancel">Отмена</string> <string name="lbl_cancel">Отмена</string>
<string name="set_pre_amp_warning">Внимание: Изменение предусиления на большое положительное значение может привести к появлению искажений на некоторых звуковых дорожках.</string> <string name="set_pre_amp_warning">Внимание: Изменение предусиления на большое положительное значение может привести к появлению искажений на некоторых звуковых дорожках.</string>
<string name="lbl_song_detail">Свойства</string> <string name="lbl_song_detail">Сведения</string>
<string name="lbl_props">Свойства песни</string> <string name="lbl_props">Свойства трека</string>
<string name="lbl_relative_path">Путь</string> <string name="lbl_relative_path">Путь</string>
<string name="lbl_format">Формат</string> <string name="lbl_format">Формат</string>
<string name="lbl_size">Размер</string> <string name="lbl_size">Размер</string>
@ -258,8 +258,6 @@
<string name="err_did_not_wipe">Не удалось очистить состояние</string> <string name="err_did_not_wipe">Не удалось очистить состояние</string>
<string name="err_did_not_save">Не удалось сохранить состояние</string> <string name="err_did_not_save">Не удалось сохранить состояние</string>
<string name="set_separators_warning">Предупреждение: Использование этой настройки может привести к тому, что некоторые теги будут неправильно интерпретироваться как имеющие несколько значений. Вы можете решить эту проблему, добавив к нежелательным символам-разделителям обратную косую черту (\\).</string> <string name="set_separators_warning">Предупреждение: Использование этой настройки может привести к тому, что некоторые теги будут неправильно интерпретироваться как имеющие несколько значений. Вы можете решить эту проблему, добавив к нежелательным символам-разделителям обратную косую черту (\\).</string>
<string name="lbl_play_selected">Воспроизвести выбранное</string>
<string name="lbl_shuffle_selected">Перемешать выбранное</string>
<string name="fmt_selected">%d выбрано</string> <string name="fmt_selected">%d выбрано</string>
<string name="lbl_wiki">Вики</string> <string name="lbl_wiki">Вики</string>
<string name="lbl_reset">Сбросить</string> <string name="lbl_reset">Сбросить</string>
@ -304,7 +302,11 @@
<string name="lbl_share">Поделиться</string> <string name="lbl_share">Поделиться</string>
<string name="set_square_covers">Использовать квадратные обложки альбомов</string> <string name="set_square_covers">Использовать квадратные обложки альбомов</string>
<string name="set_square_covers_desc">Обрезать все обложки альбомов до соотношения сторон 1:1</string> <string name="set_square_covers_desc">Обрезать все обложки альбомов до соотношения сторон 1:1</string>
<string name="lbl_song">Песня</string> <string name="lbl_song">Трек</string>
<string name="lbl_parent_detail">Вид</string> <string name="lbl_parent_detail">Вид</string>
<string name="set_play_song_by_itself">Воспроизвести трек отдельно</string> <string name="set_play_song_by_itself">Воспроизвести трек отдельно</string>
<string name="lbl_sort_mode">Сортировать по</string>
<string name="lbl_sort_direction">Направление</string>
<string name="lbl_selection">Выберите</string>
<string name="desc_selection_image">Выберите изображение</string>
</resources> </resources>

Some files were not shown because too many files have changed in this diff Show more