{
+ var code = -1
+ var msg = exception?.message
+ if (exception is HttpException) {
+ code = exception.code()
+ msg = exception.message()
+ val body = exception.response()?.errorBody()?.string().orEmpty()
+ if (body.isNotEmpty()) {
+ kotlin.runCatching {
+ val json = GsonUtils.fromJson(body, JsonObject::class.java)
+ if (json.has(codeField)) {
+ code = json.get(codeField).asInt
+ }
+ if (json.has(msgField)) {
+ msg = json.get(msgField).asString
+ }
+ }
+ }
+ }
+ return CommonResult.fail(code, msg)
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/net/datasource/MusicDataSource.java b/AAmusic/app/src/main/java/me/wcy/music/net/datasource/MusicDataSource.java
new file mode 100644
index 0000000..b41cd1a
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/net/datasource/MusicDataSource.java
@@ -0,0 +1,438 @@
+/*
+ * Copyright (C) 2016 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 me.wcy.music.net.datasource;
+
+import static me.wcy.music.utils.ModelExKt.SCHEME_NETEASE;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+
+import androidx.annotation.Nullable;
+import androidx.media3.common.util.Assertions;
+import androidx.media3.common.util.Log;
+import androidx.media3.common.util.UnstableApi;
+import androidx.media3.common.util.Util;
+import androidx.media3.datasource.AssetDataSource;
+import androidx.media3.datasource.ContentDataSource;
+import androidx.media3.datasource.DataSchemeDataSource;
+import androidx.media3.datasource.DataSource;
+import androidx.media3.datasource.DataSpec;
+import androidx.media3.datasource.DefaultDataSource;
+import androidx.media3.datasource.DefaultHttpDataSource;
+import androidx.media3.datasource.FileDataSource;
+import androidx.media3.datasource.HttpDataSource;
+import androidx.media3.datasource.RawResourceDataSource;
+import androidx.media3.datasource.TransferListener;
+import androidx.media3.datasource.UdpDataSource;
+
+//import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.firebase.crashlytics.buildtools.reloc.com.google.errorprone.annotations.CanIgnoreReturnValue;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import me.wcy.music.net.HttpClient;
+import okhttp3.Call;
+
+/**
+ *
+ * A copy of {@link DefaultDataSource} which support get real url when play.
+ *
+ *
+ * A {@link DataSource} that supports multiple URI schemes. The supported schemes are:
+ *
+ *
+ * - {@code file}: For fetching data from a local file (e.g. {@code
+ * file:///path/to/media/media.mp4}, or just {@code /path/to/media/media.mp4} because the
+ * implementation assumes that a URI without a scheme is a local file URI).
+ *
- {@code asset}: For fetching data from an asset in the application's APK (e.g. {@code
+ * asset:///media.mp4}).
+ *
- {@code rawresource}: For fetching data from a raw resource in the application's APK
+ * (e.g. {@code rawresource:///resourceId}, where {@code rawResourceId} is the integer
+ * identifier of the raw resource).
+ *
- {@code android.resource}: For fetching data in the application's APK (e.g. {@code
+ * android.resource:///resourceId} or {@code android.resource://resourceType/resourceName}).
+ * See {@link RawResourceDataSource} for more information about the URI form.
+ *
- {@code content}: For fetching data from a content URI (e.g. {@code
+ * content://authority/path/123}).
+ *
- {@code rtmp}: For fetching data over RTMP. Only supported if the project using
+ * ExoPlayer has an explicit dependency on ExoPlayer's RTMP extension.
+ *
- {@code data}: For parsing data inlined in the URI as defined in RFC 2397.
+ *
- {@code udp}: For fetching data over UDP (e.g. {@code udp://something.com/media}).
+ *
- {@code http(s)}: For fetching data over HTTP and HTTPS (e.g. {@code
+ * https://www.something.com/media.mp4}), if constructed using {@link
+ * #MusicDataSource(Context, String, boolean)}, or any other schemes supported by a base
+ * data source if constructed using {@link #MusicDataSource(Context, DataSource)}.
+ *
+ */
+@UnstableApi
+public final class MusicDataSource implements DataSource {
+
+ /**
+ * {@link DataSource.Factory} for {@link MusicDataSource} instances.
+ */
+ public static final class Factory implements DataSource.Factory {
+
+ private final Context context;
+ private final DataSource.Factory baseDataSourceFactory;
+ @Nullable
+ private TransferListener transferListener;
+
+ /**
+ * Creates an instance.
+ *
+ * @param context A context.
+ */
+ public Factory(Context context) {
+ this(context, new DefaultHttpDataSource.Factory());
+ }
+
+ /**
+ * Creates an instance.
+ *
+ * @param context A context.
+ * @param baseDataSourceFactory The {@link DataSource.Factory} to be used to create base {@link
+ * DataSource DataSources} for {@link MusicDataSource} instances. The base {@link
+ * DataSource} is normally an {@link HttpDataSource}, and is responsible for fetching data
+ * over HTTP and HTTPS, as well as any other URI schemes not otherwise supported by {@link
+ * MusicDataSource}.
+ */
+ public Factory(Context context, DataSource.Factory baseDataSourceFactory) {
+ this.context = context.getApplicationContext();
+ this.baseDataSourceFactory = baseDataSourceFactory;
+ }
+
+ /**
+ * Sets the {@link TransferListener} that will be used.
+ *
+ * The default is {@code null}.
+ *
+ *
See {@link DataSource#addTransferListener(TransferListener)}.
+ *
+ * @param transferListener The listener that will be used.
+ * @return This factory.
+ */
+ @CanIgnoreReturnValue
+ @UnstableApi
+ public Factory setTransferListener(@Nullable TransferListener transferListener) {
+ this.transferListener = transferListener;
+ return this;
+ }
+
+ @UnstableApi
+ @Override
+ public MusicDataSource createDataSource() {
+ MusicDataSource dataSource =
+ new MusicDataSource(context, baseDataSourceFactory.createDataSource());
+ if (transferListener != null) {
+ dataSource.addTransferListener(transferListener);
+ }
+ return dataSource;
+ }
+ }
+
+ private static final String TAG = "DefaultDataSource";
+
+ private static final String SCHEME_ASSET = "asset";
+ private static final String SCHEME_CONTENT = "content";
+ private static final String SCHEME_RTMP = "rtmp";
+ private static final String SCHEME_UDP = "udp";
+ private static final String SCHEME_DATA = DataSchemeDataSource.SCHEME_DATA;
+
+ @SuppressWarnings("deprecation") // Detecting deprecated scheme.
+ private static final String SCHEME_RAW = RawResourceDataSource.RAW_RESOURCE_SCHEME;
+
+ private static final String SCHEME_ANDROID_RESOURCE = ContentResolver.SCHEME_ANDROID_RESOURCE;
+
+ private final Context context;
+ private final List transferListeners;
+ private final DataSource baseDataSource;
+
+ // Lazily initialized.
+ @Nullable
+ private DataSource fileDataSource;
+ @Nullable
+ private DataSource assetDataSource;
+ @Nullable
+ private DataSource contentDataSource;
+ @Nullable
+ private DataSource rtmpDataSource;
+ @Nullable
+ private DataSource udpDataSource;
+ @Nullable
+ private DataSource dataSchemeDataSource;
+ @Nullable
+ private DataSource rawResourceDataSource;
+ @Nullable
+ private DataSource neteaseDataSource;
+
+ @Nullable
+ private DataSource dataSource;
+
+ /**
+ * Constructs a new instance, optionally configured to follow cross-protocol redirects.
+ *
+ * @param context A context.
+ * @param allowCrossProtocolRedirects Whether to allow cross-protocol redirects.
+ */
+ @UnstableApi
+ public MusicDataSource(Context context, boolean allowCrossProtocolRedirects) {
+ this(
+ context,
+ /* userAgent= */ null,
+ DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
+ allowCrossProtocolRedirects);
+ }
+
+ /**
+ * Constructs a new instance, optionally configured to follow cross-protocol redirects.
+ *
+ * @param context A context.
+ * @param userAgent The user agent that will be used when requesting remote data, or {@code null}
+ * to use the default user agent of the underlying platform.
+ * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
+ * to HTTPS and vice versa) are enabled when fetching remote data.
+ */
+ @UnstableApi
+ public MusicDataSource(
+ Context context, @Nullable String userAgent, boolean allowCrossProtocolRedirects) {
+ this(
+ context,
+ userAgent,
+ DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
+ allowCrossProtocolRedirects);
+ }
+
+ /**
+ * Constructs a new instance, optionally configured to follow cross-protocol redirects.
+ *
+ * @param context A context.
+ * @param userAgent The user agent that will be used when requesting remote data, or {@code null}
+ * to use the default user agent of the underlying platform.
+ * @param connectTimeoutMillis The connection timeout that should be used when requesting remote
+ * data, in milliseconds. A timeout of zero is interpreted as an infinite timeout.
+ * @param readTimeoutMillis The read timeout that should be used when requesting remote data, in
+ * milliseconds. A timeout of zero is interpreted as an infinite timeout.
+ * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
+ * to HTTPS and vice versa) are enabled when fetching remote data.
+ */
+ @UnstableApi
+ public MusicDataSource(
+ Context context,
+ @Nullable String userAgent,
+ int connectTimeoutMillis,
+ int readTimeoutMillis,
+ boolean allowCrossProtocolRedirects) {
+ this(
+ context,
+ new DefaultHttpDataSource.Factory()
+ .setUserAgent(userAgent)
+ .setConnectTimeoutMs(connectTimeoutMillis)
+ .setReadTimeoutMs(readTimeoutMillis)
+ .setAllowCrossProtocolRedirects(allowCrossProtocolRedirects)
+ .createDataSource());
+ }
+
+ /**
+ * Constructs a new instance that delegates to a provided {@link DataSource} for URI schemes other
+ * than file, asset and content.
+ *
+ * @param context A context.
+ * @param baseDataSource A {@link DataSource} to use for URI schemes other than file, asset and
+ * content. This {@link DataSource} should normally support at least http(s).
+ */
+ @UnstableApi
+ public MusicDataSource(Context context, DataSource baseDataSource) {
+ this.context = context.getApplicationContext();
+ this.baseDataSource = Assertions.checkNotNull(baseDataSource);
+ transferListeners = new ArrayList<>();
+ }
+
+ @UnstableApi
+ @Override
+ public void addTransferListener(TransferListener transferListener) {
+ Assertions.checkNotNull(transferListener);
+ baseDataSource.addTransferListener(transferListener);
+ transferListeners.add(transferListener);
+ maybeAddListenerToDataSource(fileDataSource, transferListener);
+ maybeAddListenerToDataSource(assetDataSource, transferListener);
+ maybeAddListenerToDataSource(contentDataSource, transferListener);
+ maybeAddListenerToDataSource(rtmpDataSource, transferListener);
+ maybeAddListenerToDataSource(udpDataSource, transferListener);
+ maybeAddListenerToDataSource(dataSchemeDataSource, transferListener);
+ maybeAddListenerToDataSource(rawResourceDataSource, transferListener);
+ }
+
+ @UnstableApi
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ Assertions.checkState(dataSource == null);
+ // Choose the correct source for the scheme.
+ String scheme = dataSpec.uri.getScheme();
+ if (Util.isLocalFileUri(dataSpec.uri)) {
+ String uriPath = dataSpec.uri.getPath();
+ if (uriPath != null && uriPath.startsWith("/android_asset/")) {
+ dataSource = getAssetDataSource();
+ } else {
+ dataSource = getFileDataSource();
+ }
+ } else if (SCHEME_ASSET.equals(scheme)) {
+ dataSource = getAssetDataSource();
+ } else if (SCHEME_CONTENT.equals(scheme)) {
+ dataSource = getContentDataSource();
+ } else if (SCHEME_RTMP.equals(scheme)) {
+ dataSource = getRtmpDataSource();
+ } else if (SCHEME_UDP.equals(scheme)) {
+ dataSource = getUdpDataSource();
+ } else if (SCHEME_DATA.equals(scheme)) {
+ dataSource = getDataSchemeDataSource();
+ } else if (SCHEME_RAW.equals(scheme) || SCHEME_ANDROID_RESOURCE.equals(scheme)) {
+ dataSource = getRawResourceDataSource();
+ } else if (SCHEME_NETEASE.equals(scheme)) {
+ dataSource = getNeteaseDataSource();
+ } else {
+ dataSource = baseDataSource;
+ }
+ // Open the source and return.
+ return dataSource.open(dataSpec);
+ }
+
+ @UnstableApi
+ @Override
+ public int read(byte[] buffer, int offset, int length) throws IOException {
+ return Assertions.checkNotNull(dataSource).read(buffer, offset, length);
+ }
+
+ @UnstableApi
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return dataSource == null ? null : dataSource.getUri();
+ }
+
+ @UnstableApi
+ @Override
+ public Map> getResponseHeaders() {
+ return dataSource == null ? Collections.emptyMap() : dataSource.getResponseHeaders();
+ }
+
+ @UnstableApi
+ @Override
+ public void close() throws IOException {
+ if (dataSource != null) {
+ try {
+ dataSource.close();
+ } finally {
+ dataSource = null;
+ }
+ }
+ }
+
+ private DataSource getUdpDataSource() {
+ if (udpDataSource == null) {
+ udpDataSource = new UdpDataSource();
+ addListenersToDataSource(udpDataSource);
+ }
+ return udpDataSource;
+ }
+
+ private DataSource getFileDataSource() {
+ if (fileDataSource == null) {
+ fileDataSource = new FileDataSource();
+ addListenersToDataSource(fileDataSource);
+ }
+ return fileDataSource;
+ }
+
+ private DataSource getAssetDataSource() {
+ if (assetDataSource == null) {
+ assetDataSource = new AssetDataSource(context);
+ addListenersToDataSource(assetDataSource);
+ }
+ return assetDataSource;
+ }
+
+ private DataSource getContentDataSource() {
+ if (contentDataSource == null) {
+ contentDataSource = new ContentDataSource(context);
+ addListenersToDataSource(contentDataSource);
+ }
+ return contentDataSource;
+ }
+
+ private DataSource getRtmpDataSource() {
+ if (rtmpDataSource == null) {
+ try {
+ Class> clazz = Class.forName("androidx.media3.datasource.rtmp.RtmpDataSource");
+ rtmpDataSource = (DataSource) clazz.getConstructor().newInstance();
+ addListenersToDataSource(rtmpDataSource);
+ } catch (ClassNotFoundException e) {
+ // Expected if the app was built without the RTMP extension.
+ Log.w(TAG, "Attempting to play RTMP stream without depending on the RTMP extension");
+ } catch (Exception e) {
+ // The RTMP extension is present, but instantiation failed.
+ throw new RuntimeException("Error instantiating RTMP extension", e);
+ }
+ if (rtmpDataSource == null) {
+ rtmpDataSource = baseDataSource;
+ }
+ }
+ return rtmpDataSource;
+ }
+
+ private DataSource getDataSchemeDataSource() {
+ if (dataSchemeDataSource == null) {
+ dataSchemeDataSource = new DataSchemeDataSource();
+ addListenersToDataSource(dataSchemeDataSource);
+ }
+ return dataSchemeDataSource;
+ }
+
+ private DataSource getRawResourceDataSource() {
+ if (rawResourceDataSource == null) {
+ rawResourceDataSource = new RawResourceDataSource(context);
+ addListenersToDataSource(rawResourceDataSource);
+ }
+ return rawResourceDataSource;
+ }
+
+ private DataSource getNeteaseDataSource() {
+ if (neteaseDataSource == null) {
+ neteaseDataSource = new OnlineMusicDataSource((Call.Factory) HttpClient.INSTANCE.getOkHttpClient());
+ addListenersToDataSource(neteaseDataSource);
+ }
+ return neteaseDataSource;
+ }
+
+ private void addListenersToDataSource(DataSource dataSource) {
+ for (int i = 0; i < transferListeners.size(); i++) {
+ dataSource.addTransferListener(transferListeners.get(i));
+ }
+ }
+
+ private void maybeAddListenerToDataSource(
+ @Nullable DataSource dataSource, TransferListener listener) {
+ if (dataSource != null) {
+ dataSource.addTransferListener(listener);
+ }
+ }
+}
diff --git a/AAmusic/app/src/main/java/me/wcy/music/net/datasource/OnlineMusicDataSource.java b/AAmusic/app/src/main/java/me/wcy/music/net/datasource/OnlineMusicDataSource.java
new file mode 100644
index 0000000..5ae1c9b
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/net/datasource/OnlineMusicDataSource.java
@@ -0,0 +1,614 @@
+/*
+ * Copyright (C) 2016 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 me.wcy.music.net.datasource;
+
+import static androidx.media3.common.util.Util.castNonNull;
+import static androidx.media3.datasource.HttpUtil.buildRangeRequestHeader;
+import static java.lang.Math.min;
+
+import android.net.Uri;
+import android.text.TextUtils;
+
+import androidx.annotation.Nullable;
+import androidx.media3.common.C;
+import androidx.media3.common.MediaLibraryInfo;
+import androidx.media3.common.PlaybackException;
+import androidx.media3.common.util.Assertions;
+import androidx.media3.common.util.UnstableApi;
+import androidx.media3.common.util.Util;
+import androidx.media3.datasource.BaseDataSource;
+import androidx.media3.datasource.DataSource;
+import androidx.media3.datasource.DataSourceException;
+import androidx.media3.datasource.DataSpec;
+import androidx.media3.datasource.HttpDataSource;
+import androidx.media3.datasource.HttpUtil;
+import androidx.media3.datasource.TransferListener;
+import androidx.media3.datasource.okhttp.OkHttpDataSource;
+
+import com.google.common.base.Predicate;
+import com.google.common.net.HttpHeaders;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+import okhttp3.CacheControl;
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.HttpUrl;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+/**
+ *
+ * A copy of {@link OkHttpDataSource} which support get real url when play.
+ *
+ * An {@link HttpDataSource} that delegates to Square's {@link Call.Factory}.
+ *
+ * Note: HTTP request headers will be set using all parameters passed via (in order of decreasing
+ * priority) the {@code dataSpec}, {@link #setRequestProperty} and the default parameters used to
+ * construct the instance.
+ */
+@UnstableApi
+public class OnlineMusicDataSource extends BaseDataSource implements HttpDataSource {
+
+ static {
+ MediaLibraryInfo.registerModule("media3.datasource.okhttp");
+ }
+
+ /**
+ * {@link DataSource.Factory} for {@link OnlineMusicDataSource} instances.
+ */
+ public static final class Factory implements HttpDataSource.Factory {
+
+ private final RequestProperties defaultRequestProperties;
+ private final Call.Factory callFactory;
+
+ @Nullable
+ private String userAgent;
+ @Nullable
+ private TransferListener transferListener;
+ @Nullable
+ private CacheControl cacheControl;
+ @Nullable
+ private Predicate contentTypePredicate;
+
+ /**
+ * Creates an instance.
+ *
+ * @param callFactory A {@link Call.Factory} (typically an {@link OkHttpClient}) for use by the
+ * sources created by the factory.
+ */
+ public Factory(Call.Factory callFactory) {
+ this.callFactory = callFactory;
+ defaultRequestProperties = new RequestProperties();
+ }
+
+ @CanIgnoreReturnValue
+ @UnstableApi
+ @Override
+ public final Factory setDefaultRequestProperties(Map defaultRequestProperties) {
+ this.defaultRequestProperties.clearAndSet(defaultRequestProperties);
+ return this;
+ }
+
+ /**
+ * Sets the user agent that will be used.
+ *
+ * The default is {@code null}, which causes the default user agent of the underlying {@link
+ * OkHttpClient} to be used.
+ *
+ * @param userAgent The user agent that will be used, or {@code null} to use the default user
+ * agent of the underlying {@link OkHttpClient}.
+ * @return This factory.
+ */
+ @CanIgnoreReturnValue
+ @UnstableApi
+ public Factory setUserAgent(@Nullable String userAgent) {
+ this.userAgent = userAgent;
+ return this;
+ }
+
+ /**
+ * Sets the {@link CacheControl} that will be used.
+ *
+ *
The default is {@code null}.
+ *
+ * @param cacheControl The cache control that will be used.
+ * @return This factory.
+ */
+ @CanIgnoreReturnValue
+ @UnstableApi
+ public Factory setCacheControl(@Nullable CacheControl cacheControl) {
+ this.cacheControl = cacheControl;
+ return this;
+ }
+
+ /**
+ * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
+ * {@link InvalidContentTypeException} is thrown from {@link
+ * OnlineMusicDataSource#open(DataSpec)}.
+ *
+ *
The default is {@code null}.
+ *
+ * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
+ * predicate that was previously set.
+ * @return This factory.
+ */
+ @CanIgnoreReturnValue
+ @UnstableApi
+ public Factory setContentTypePredicate(@Nullable Predicate contentTypePredicate) {
+ this.contentTypePredicate = contentTypePredicate;
+ return this;
+ }
+
+ /**
+ * Sets the {@link TransferListener} that will be used.
+ *
+ * The default is {@code null}.
+ *
+ *
See {@link DataSource#addTransferListener(TransferListener)}.
+ *
+ * @param transferListener The listener that will be used.
+ * @return This factory.
+ */
+ @CanIgnoreReturnValue
+ @UnstableApi
+ public Factory setTransferListener(@Nullable TransferListener transferListener) {
+ this.transferListener = transferListener;
+ return this;
+ }
+
+ @UnstableApi
+ @Override
+ public OnlineMusicDataSource createDataSource() {
+ OnlineMusicDataSource dataSource =
+ new OnlineMusicDataSource(
+ callFactory, userAgent, cacheControl, defaultRequestProperties, contentTypePredicate);
+ if (transferListener != null) {
+ dataSource.addTransferListener(transferListener);
+ }
+ return dataSource;
+ }
+ }
+
+ private final Call.Factory callFactory;
+ private final RequestProperties requestProperties;
+
+ @Nullable
+ private final String userAgent;
+ @Nullable
+ private final CacheControl cacheControl;
+ @Nullable
+ private final RequestProperties defaultRequestProperties;
+
+ @Nullable
+ private Predicate contentTypePredicate;
+ @Nullable
+ private DataSpec dataSpec;
+ @Nullable
+ private Response response;
+ @Nullable
+ private InputStream responseByteStream;
+ private boolean opened;
+ private long bytesToRead;
+ private long bytesRead;
+
+ /**
+ * @deprecated Use {@link OnlineMusicDataSource.Factory} instead.
+ */
+ @SuppressWarnings("deprecation")
+ @UnstableApi
+ @Deprecated
+ public OnlineMusicDataSource(Call.Factory callFactory) {
+ this(callFactory, /* userAgent= */ null);
+ }
+
+ /**
+ * @deprecated Use {@link OnlineMusicDataSource.Factory} instead.
+ */
+ @SuppressWarnings("deprecation")
+ @UnstableApi
+ @Deprecated
+ public OnlineMusicDataSource(Call.Factory callFactory, @Nullable String userAgent) {
+ this(callFactory, userAgent, /* cacheControl= */ null, /* defaultRequestProperties= */ null);
+ }
+
+ /**
+ * @deprecated Use {@link OnlineMusicDataSource.Factory} instead.
+ */
+ @UnstableApi
+ @Deprecated
+ public OnlineMusicDataSource(
+ Call.Factory callFactory,
+ @Nullable String userAgent,
+ @Nullable CacheControl cacheControl,
+ @Nullable RequestProperties defaultRequestProperties) {
+ this(
+ callFactory,
+ userAgent,
+ cacheControl,
+ defaultRequestProperties,
+ /* contentTypePredicate= */ null);
+ }
+
+ private OnlineMusicDataSource(
+ Call.Factory callFactory,
+ @Nullable String userAgent,
+ @Nullable CacheControl cacheControl,
+ @Nullable RequestProperties defaultRequestProperties,
+ @Nullable Predicate contentTypePredicate) {
+ super(/* isNetwork= */ true);
+ this.callFactory = Assertions.checkNotNull(callFactory);
+ this.userAgent = userAgent;
+ this.cacheControl = cacheControl;
+ this.defaultRequestProperties = defaultRequestProperties;
+ this.contentTypePredicate = contentTypePredicate;
+ this.requestProperties = new RequestProperties();
+ }
+
+ /**
+ * @deprecated Use {@link OnlineMusicDataSource.Factory#setContentTypePredicate(Predicate)} instead.
+ */
+ @UnstableApi
+ @Deprecated
+ public void setContentTypePredicate(@Nullable Predicate contentTypePredicate) {
+ this.contentTypePredicate = contentTypePredicate;
+ }
+
+ @UnstableApi
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return response == null ? null : Uri.parse(response.request().url().toString());
+ }
+
+ @UnstableApi
+ @Override
+ public int getResponseCode() {
+ return response == null ? -1 : response.code();
+ }
+
+ @UnstableApi
+ @Override
+ public Map> getResponseHeaders() {
+ return response == null ? Collections.emptyMap() : response.headers().toMultimap();
+ }
+
+ @UnstableApi
+ @Override
+ public void setRequestProperty(String name, String value) {
+ Assertions.checkNotNull(name);
+ Assertions.checkNotNull(value);
+ requestProperties.set(name, value);
+ }
+
+ @UnstableApi
+ @Override
+ public void clearRequestProperty(String name) {
+ Assertions.checkNotNull(name);
+ requestProperties.remove(name);
+ }
+
+ @UnstableApi
+ @Override
+ public void clearAllRequestProperties() {
+ requestProperties.clear();
+ }
+
+ @UnstableApi
+ @Override
+ public long open(DataSpec dataSpec) throws HttpDataSourceException {
+ this.dataSpec = dataSpec;
+ bytesRead = 0;
+ bytesToRead = 0;
+ transferInitializing(dataSpec);
+
+ Request request = makeRequest(dataSpec);
+ Response response;
+ ResponseBody responseBody;
+ Call call = callFactory.newCall(request);
+ try {
+ this.response = executeCall(call);
+ response = this.response;
+ responseBody = Assertions.checkNotNull(response.body());
+ responseByteStream = responseBody.byteStream();
+ } catch (IOException e) {
+ throw HttpDataSourceException.createForIOException(
+ e, dataSpec, HttpDataSourceException.TYPE_OPEN);
+ }
+
+ int responseCode = response.code();
+
+ // Check for a valid response code.
+ if (!response.isSuccessful()) {
+ if (responseCode == 416) {
+ long documentSize =
+ HttpUtil.getDocumentSize(response.headers().get(HttpHeaders.CONTENT_RANGE));
+ if (dataSpec.position == documentSize) {
+ opened = true;
+ transferStarted(dataSpec);
+ return dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : 0;
+ }
+ }
+
+ byte[] errorResponseBody;
+ try {
+ errorResponseBody = Util.toByteArray(Assertions.checkNotNull(responseByteStream));
+ } catch (IOException e) {
+ errorResponseBody = Util.EMPTY_BYTE_ARRAY;
+ }
+ Map> headers = response.headers().toMultimap();
+ closeConnectionQuietly();
+ @Nullable
+ IOException cause =
+ responseCode == 416
+ ? new DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE)
+ : null;
+ throw new InvalidResponseCodeException(
+ responseCode, response.message(), cause, headers, dataSpec, errorResponseBody);
+ }
+
+ // Check for a valid content type.
+ @Nullable MediaType mediaType = responseBody.contentType();
+ String contentType = mediaType != null ? mediaType.toString() : "";
+ if (contentTypePredicate != null && !contentTypePredicate.apply(contentType)) {
+ closeConnectionQuietly();
+ throw new InvalidContentTypeException(contentType, dataSpec);
+ }
+
+ // If we requested a range starting from a non-zero position and received a 200 rather than a
+ // 206, then the server does not support partial requests. We'll need to manually skip to the
+ // requested position.
+ long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
+
+ // Determine the length of the data to be read, after skipping.
+ if (dataSpec.length != C.LENGTH_UNSET) {
+ bytesToRead = dataSpec.length;
+ } else {
+ long contentLength = responseBody.contentLength();
+ bytesToRead = contentLength != -1 ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
+ }
+
+ opened = true;
+ transferStarted(dataSpec);
+
+ try {
+ skipFully(bytesToSkip, dataSpec);
+ } catch (HttpDataSourceException e) {
+ closeConnectionQuietly();
+ throw e;
+ }
+
+ return bytesToRead;
+ }
+
+ @UnstableApi
+ @Override
+ public int read(byte[] buffer, int offset, int length) throws HttpDataSourceException {
+ try {
+ return readInternal(buffer, offset, length);
+ } catch (IOException e) {
+ throw HttpDataSourceException.createForIOException(
+ e, castNonNull(dataSpec), HttpDataSourceException.TYPE_READ);
+ }
+ }
+
+ @UnstableApi
+ @Override
+ public void close() {
+ if (opened) {
+ opened = false;
+ transferEnded();
+ closeConnectionQuietly();
+ }
+ }
+
+ /**
+ * Establishes a connection.
+ */
+ private Request makeRequest(DataSpec dataSpec) throws HttpDataSourceException {
+ String playUrl = OnlineMusicUriFetcher.INSTANCE.fetchPlayUrl(dataSpec.uri);
+ if (TextUtils.isEmpty(playUrl)) {
+ throw new HttpDataSourceException(
+ "Request song url error",
+ dataSpec,
+ PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK,
+ HttpDataSourceException.TYPE_OPEN
+ );
+ }
+
+ long position = dataSpec.position;
+ long length = dataSpec.length;
+
+ @Nullable HttpUrl url = HttpUrl.parse(playUrl);
+ if (url == null) {
+ throw new HttpDataSourceException(
+ "Malformed URL",
+ dataSpec,
+ PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK,
+ HttpDataSourceException.TYPE_OPEN);
+ }
+
+ Request.Builder builder = new Request.Builder().url(url);
+ if (cacheControl != null) {
+ builder.cacheControl(cacheControl);
+ }
+
+ Map headers = new HashMap<>();
+ if (defaultRequestProperties != null) {
+ headers.putAll(defaultRequestProperties.getSnapshot());
+ }
+
+ headers.putAll(requestProperties.getSnapshot());
+ headers.putAll(dataSpec.httpRequestHeaders);
+
+ for (Map.Entry header : headers.entrySet()) {
+ builder.header(header.getKey(), header.getValue());
+ }
+
+ @Nullable String rangeHeader = buildRangeRequestHeader(position, length);
+ if (rangeHeader != null) {
+ builder.addHeader(HttpHeaders.RANGE, rangeHeader);
+ }
+ if (userAgent != null) {
+ builder.addHeader(HttpHeaders.USER_AGENT, userAgent);
+ }
+ if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
+ builder.addHeader(HttpHeaders.ACCEPT_ENCODING, "identity");
+ }
+
+ @Nullable RequestBody requestBody = null;
+ if (dataSpec.httpBody != null) {
+ requestBody = RequestBody.create(dataSpec.httpBody);
+ } else if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
+ // OkHttp requires a non-null body for POST requests.
+ requestBody = RequestBody.create(Util.EMPTY_BYTE_ARRAY);
+ }
+ builder.method(dataSpec.getHttpMethodString(), requestBody);
+ return builder.build();
+ }
+
+ /**
+ * This method is an interrupt safe replacement of OkHttp Call.execute() which can get in bad
+ * states if interrupted while writing to the shared connection socket.
+ */
+ private Response executeCall(Call call) throws IOException {
+ SettableFuture future = SettableFuture.create();
+ call.enqueue(
+ new Callback() {
+ @Override
+ public void onFailure(Call call, IOException e) {
+ future.setException(e);
+ }
+
+ @Override
+ public void onResponse(Call call, Response response) {
+ future.set(response);
+ }
+ });
+
+ try {
+ return future.get();
+ } catch (InterruptedException e) {
+ call.cancel();
+ throw new InterruptedIOException();
+ } catch (ExecutionException ee) {
+ throw new IOException(ee);
+ }
+ }
+
+ /**
+ * Attempts to skip the specified number of bytes in full.
+ *
+ * @param bytesToSkip The number of bytes to skip.
+ * @param dataSpec The {@link DataSpec}.
+ * @throws HttpDataSourceException If the thread is interrupted during the operation, or an error
+ * occurs while reading from the source, or if the data ended before skipping the specified
+ * number of bytes.
+ */
+ private void skipFully(long bytesToSkip, DataSpec dataSpec) throws HttpDataSourceException {
+ if (bytesToSkip == 0) {
+ return;
+ }
+ byte[] skipBuffer = new byte[4096];
+ try {
+ while (bytesToSkip > 0) {
+ int readLength = (int) min(bytesToSkip, skipBuffer.length);
+ int read = castNonNull(responseByteStream).read(skipBuffer, 0, readLength);
+ if (Thread.currentThread().isInterrupted()) {
+ throw new InterruptedIOException();
+ }
+ if (read == -1) {
+ throw new HttpDataSourceException(
+ dataSpec,
+ PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE,
+ HttpDataSourceException.TYPE_OPEN);
+ }
+ bytesToSkip -= read;
+ bytesTransferred(read);
+ }
+ return;
+ } catch (IOException e) {
+ if (e instanceof HttpDataSourceException) {
+ throw (HttpDataSourceException) e;
+ } else {
+ throw new HttpDataSourceException(
+ dataSpec,
+ PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
+ HttpDataSourceException.TYPE_OPEN);
+ }
+ }
+ }
+
+ /**
+ * Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at index
+ * {@code offset}.
+ *
+ * This method blocks until at least one byte of data can be read, the end of the opened range
+ * is detected, or an exception is thrown.
+ *
+ * @param buffer The buffer into which the read data should be stored.
+ * @param offset The start offset into {@code buffer} at which data should be written.
+ * @param readLength The maximum number of bytes to read.
+ * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the end of the opened
+ * range is reached.
+ * @throws IOException If an error occurs reading from the source.
+ */
+ private int readInternal(byte[] buffer, int offset, int readLength) throws IOException {
+ if (readLength == 0) {
+ return 0;
+ }
+ if (bytesToRead != C.LENGTH_UNSET) {
+ long bytesRemaining = bytesToRead - bytesRead;
+ if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ readLength = (int) min(readLength, bytesRemaining);
+ }
+
+ int read = castNonNull(responseByteStream).read(buffer, offset, readLength);
+ if (read == -1) {
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ bytesRead += read;
+ bytesTransferred(read);
+ return read;
+ }
+
+ /**
+ * Closes the current connection quietly, if there is one.
+ */
+ private void closeConnectionQuietly() {
+ if (response != null) {
+ Assertions.checkNotNull(response.body()).close();
+ response = null;
+ }
+ responseByteStream = null;
+ }
+}
diff --git a/AAmusic/app/src/main/java/me/wcy/music/net/datasource/OnlineMusicUriFetcher.kt b/AAmusic/app/src/main/java/me/wcy/music/net/datasource/OnlineMusicUriFetcher.kt
new file mode 100644
index 0000000..1df07bf
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/net/datasource/OnlineMusicUriFetcher.kt
@@ -0,0 +1,29 @@
+package me.wcy.music.net.datasource
+
+import android.net.Uri
+import kotlinx.coroutines.runBlocking
+import me.wcy.music.discover.DiscoverApi
+import me.wcy.music.storage.preference.ConfigPreferences
+import top.wangchenyan.common.net.apiCall
+
+/**
+ *
+ */
+object OnlineMusicUriFetcher {
+
+ fun fetchPlayUrl(uri: Uri): String {
+ val songId = uri.getQueryParameter("id")?.toLongOrNull() ?: return uri.toString()
+ return runBlocking {
+ val res = apiCall {
+ DiscoverApi.get()
+ .getSongUrl(songId, ConfigPreferences.playSoundQuality)
+ }
+
+ if (res.isSuccessWithData() && res.getDataOrThrow().isNotEmpty()) {
+ return@runBlocking res.getDataOrThrow().first().url
+ } else {
+ return@runBlocking ""
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/search/SearchApi.kt b/AAmusic/app/src/main/java/me/wcy/music/search/SearchApi.kt
new file mode 100644
index 0000000..f454fb6
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/search/SearchApi.kt
@@ -0,0 +1,53 @@
+package me.wcy.music.search
+
+import top.wangchenyan.common.net.NetResult
+import top.wangchenyan.common.net.gson.GsonConverterFactory
+import top.wangchenyan.common.utils.GsonUtils
+import me.wcy.music.net.HttpClient
+import me.wcy.music.search.bean.SearchResultData
+import me.wcy.music.storage.preference.ConfigPreferences
+import retrofit2.Retrofit
+import retrofit2.http.POST
+import retrofit2.http.Query
+
+/**
+ *
+ */
+interface SearchApi {
+
+ /**
+ * 搜索歌曲
+ * @param type 搜索类型;默认为 1 即单曲 , 取值意义 :
+ * - 1: 单曲,
+ * - 10: 专辑,
+ * - 100: 歌手,
+ * - 1000: 歌单,
+ * - 1002: 用户,
+ * - 1004: MV,
+ * - 1006: 歌词,
+ * - 1009: 电台,
+ * - 1014: 视频,
+ * - 1018:综合,
+ * - 2000:声音(搜索声音返回字段格式会不一样)
+ */
+ @POST("cloudsearch")
+ suspend fun search(
+ @Query("type") type: Int,
+ @Query("keywords") keywords: String,
+ @Query("limit") limit: Int,
+ @Query("offset") offset: Int,
+ ): NetResult
+
+ companion object {
+ private val api: SearchApi by lazy {
+ val retrofit = Retrofit.Builder()
+ .baseUrl(ConfigPreferences.apiDomain)
+ .addConverterFactory(GsonConverterFactory.create(GsonUtils.gson, true))
+ .client(HttpClient.okHttpClient)
+ .build()
+ retrofit.create(SearchApi::class.java)
+ }
+
+ fun get(): SearchApi = api
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/search/SearchFragment.kt b/AAmusic/app/src/main/java/me/wcy/music/search/SearchFragment.kt
new file mode 100644
index 0000000..a520027
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/search/SearchFragment.kt
@@ -0,0 +1,127 @@
+package me.wcy.music.search
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import androidx.core.view.isVisible
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
+import com.blankj.utilcode.util.KeyboardUtils
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import top.wangchenyan.common.ext.viewBindings
+import top.wangchenyan.common.widget.pager.TabLayoutPager
+import me.wcy.music.R
+import me.wcy.music.common.BaseMusicFragment
+import me.wcy.music.consts.RoutePath
+import me.wcy.music.databinding.FragmentSearchBinding
+import me.wcy.music.databinding.ItemSearchHistoryBinding
+import me.wcy.music.databinding.TitleSearchBinding
+import me.wcy.music.search.playlist.SearchPlaylistFragment
+import me.wcy.music.search.song.SearchSongFragment
+import me.wcy.router.annotation.Route
+
+/**
+ *
+ */
+@Route(RoutePath.SEARCH)
+@AndroidEntryPoint
+class SearchFragment : BaseMusicFragment() {
+ private val viewBinding by viewBindings()
+ private val titleBinding by lazy {
+ TitleSearchBinding.bind(getTitleLayout()!!.getContentView()!!)
+ }
+ private val viewModel by activityViewModels()
+ private val menuSearch by lazy {
+ getTitleLayout()!!.addTextMenu("搜索", false)!!
+ }
+
+ override fun getRootView(): View {
+ return viewBinding.root
+ }
+
+ override fun onLazyCreate() {
+ super.onLazyCreate()
+
+ initTitle()
+ initTab()
+ initHistory()
+
+ lifecycleScope.launch {
+ viewModel.showResult.collectLatest { showResult ->
+ viewBinding.llHistory.isVisible = showResult.not()
+ viewBinding.llResult.isVisible = showResult
+ }
+ }
+
+ lifecycleScope.launch {
+ delay(200)
+ KeyboardUtils.showSoftInput(titleBinding.etSearch)
+ }
+ }
+
+ private fun initTitle() {
+ titleBinding.etSearch.setOnEditorActionListener { v, actionId, event ->
+ if (actionId == EditorInfo.IME_ACTION_SEARCH) {
+ menuSearch.performClick()
+ return@setOnEditorActionListener true
+ }
+ return@setOnEditorActionListener false
+ }
+ menuSearch.setOnClickListener {
+ val keywords = titleBinding.etSearch.text?.trim()?.toString() ?: ""
+ if (keywords.isNotEmpty()) {
+ KeyboardUtils.hideSoftInput(requireActivity())
+ viewModel.search(keywords)
+ }
+ }
+ }
+
+ private fun initTab() {
+ val pager = TabLayoutPager(
+ lifecycle,
+ childFragmentManager,
+ viewBinding.viewPage2,
+ viewBinding.tabLayout
+ )
+ pager.addFragment(SearchSongFragment(), "单曲")
+ pager.addFragment(SearchPlaylistFragment(), "歌单")
+ pager.setup()
+ }
+
+ private fun initHistory() {
+ lifecycleScope.launch {
+ viewModel.historyKeywords.collectLatest { list ->
+ viewBinding.flHistory.removeAllViews()
+ list.forEach { text ->
+ ItemSearchHistoryBinding.inflate(
+ LayoutInflater.from(context),
+ viewBinding.flHistory,
+ true
+ ).apply {
+ root.text = text
+ root.setOnClickListener {
+ titleBinding.etSearch.setText(text)
+ titleBinding.etSearch.setSelection(text.length)
+ menuSearch.performClick()
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun onInterceptBackEvent(): Boolean {
+ if (viewModel.showResult.value) {
+ viewModel.showHistory()
+ return true
+ }
+ return super.onInterceptBackEvent()
+ }
+
+ override fun getNavigationBarColor(): Int {
+ return R.color.play_bar_bg
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/search/SearchPreference.kt b/AAmusic/app/src/main/java/me/wcy/music/search/SearchPreference.kt
new file mode 100644
index 0000000..ad0d77d
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/search/SearchPreference.kt
@@ -0,0 +1,14 @@
+package me.wcy.music.search
+
+import top.wangchenyan.common.CommonApp
+import top.wangchenyan.common.storage.IPreferencesFile
+import top.wangchenyan.common.storage.PreferencesFile
+import me.wcy.music.consts.PreferenceName
+
+/**
+ *
+ */
+object SearchPreference :
+ IPreferencesFile by PreferencesFile(CommonApp.app, PreferenceName.SEARCH) {
+ var historyKeywords by IPreferencesFile.ListProperty("history_keywords", String::class.java)
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/search/SearchViewModel.kt b/AAmusic/app/src/main/java/me/wcy/music/search/SearchViewModel.kt
new file mode 100644
index 0000000..fc4da8e
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/search/SearchViewModel.kt
@@ -0,0 +1,44 @@
+package me.wcy.music.search
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+import top.wangchenyan.common.ext.toUnMutable
+import me.wcy.music.consts.Consts
+
+/**
+ *
+ */
+class SearchViewModel : ViewModel() {
+ private val _keywords = MutableStateFlow("")
+ val keywords = _keywords.toUnMutable()
+
+ private val _historyKeywords = MutableStateFlow(SearchPreference.historyKeywords ?: emptyList())
+ val historyKeywords = _historyKeywords.toUnMutable()
+
+ private val _showResult = MutableStateFlow(false)
+ val showResult = _showResult.toUnMutable()
+
+ fun search(keywords: String) {
+ if (keywords.isEmpty()) {
+ return
+ }
+ _keywords.value = keywords
+ _showResult.value = true
+
+ val list = _historyKeywords.value.toMutableList()
+ list.remove(keywords)
+ list.add(0, keywords)
+ val realList = list.take(Consts.SEARCH_HISTORY_COUNT)
+ _historyKeywords.value = realList
+ viewModelScope.launch(Dispatchers.IO) {
+ SearchPreference.historyKeywords = realList
+ }
+ }
+
+ fun showHistory() {
+ _showResult.value = false
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/search/bean/SearchResultData.kt b/AAmusic/app/src/main/java/me/wcy/music/search/bean/SearchResultData.kt
new file mode 100644
index 0000000..16c7061
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/search/bean/SearchResultData.kt
@@ -0,0 +1,19 @@
+package me.wcy.music.search.bean
+
+import com.google.gson.annotations.SerializedName
+import me.wcy.music.common.bean.PlaylistData
+import me.wcy.music.common.bean.SongData
+
+/**
+ *
+ */
+data class SearchResultData(
+ @SerializedName("songs")
+ val songs: List = emptyList(),
+ @SerializedName("songCount")
+ val songCount: Int = 0,
+ @SerializedName("playlists")
+ val playlists: List = emptyList(),
+ @SerializedName("playlistCount")
+ val playlistCount: Int = 0,
+)
diff --git a/AAmusic/app/src/main/java/me/wcy/music/search/playlist/SearchPlaylistFragment.kt b/AAmusic/app/src/main/java/me/wcy/music/search/playlist/SearchPlaylistFragment.kt
new file mode 100644
index 0000000..f998f84
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/search/playlist/SearchPlaylistFragment.kt
@@ -0,0 +1,76 @@
+package me.wcy.music.search.playlist
+
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import top.wangchenyan.common.model.CommonResult
+import top.wangchenyan.common.net.apiCall
+import me.wcy.music.common.SimpleMusicRefreshFragment
+import me.wcy.music.common.bean.PlaylistData
+import me.wcy.music.consts.Consts
+import me.wcy.music.consts.RoutePath
+import me.wcy.music.search.SearchApi
+import me.wcy.music.search.SearchViewModel
+import me.wcy.radapter3.RAdapter
+import me.wcy.router.CRouter
+
+/**
+ *
+ */
+@AndroidEntryPoint
+class SearchPlaylistFragment : SimpleMusicRefreshFragment() {
+ private val viewModel by activityViewModels()
+ private val itemBinder by lazy {
+ SearchPlaylistItemBinder { item ->
+ CRouter.with(requireActivity())
+ .url(RoutePath.PLAYLIST_DETAIL)
+ .extra("id", item.id)
+ .start()
+ }.apply {
+ keywords = viewModel.keywords.value
+ }
+ }
+
+ override fun isShowTitle(): Boolean {
+ return false
+ }
+
+ override fun isRefreshEnabled(): Boolean {
+ return false
+ }
+
+ override fun onLazyCreate() {
+ super.onLazyCreate()
+ lifecycleScope.launch {
+ viewModel.keywords.collectLatest {
+ if (it.isNotEmpty()) {
+ showLoadSirLoading()
+ itemBinder.keywords = it
+ autoRefresh(true)
+ }
+ }
+ }
+ }
+
+ override fun initAdapter(adapter: RAdapter) {
+ adapter.register(itemBinder)
+ }
+
+ override suspend fun getData(page: Int): CommonResult> {
+ val keywords = viewModel.keywords.value
+ if (keywords.isEmpty()) {
+ return CommonResult.success(emptyList())
+ }
+ val res = apiCall {
+ SearchApi.get()
+ .search(1000, keywords, Consts.PAGE_COUNT, (page - 1) * Consts.PAGE_COUNT)
+ }
+ return if (res.isSuccessWithData()) {
+ CommonResult.success(res.getDataOrThrow().playlists)
+ } else {
+ CommonResult.fail(res.code, res.msg)
+ }
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/search/playlist/SearchPlaylistItemBinder.kt b/AAmusic/app/src/main/java/me/wcy/music/search/playlist/SearchPlaylistItemBinder.kt
new file mode 100644
index 0000000..2ffd9a2
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/search/playlist/SearchPlaylistItemBinder.kt
@@ -0,0 +1,31 @@
+package me.wcy.music.search.playlist
+
+import android.annotation.SuppressLint
+import com.blankj.utilcode.util.SizeUtils
+import top.wangchenyan.common.ext.context
+import me.wcy.music.common.bean.PlaylistData
+import me.wcy.music.databinding.ItemSearchPlaylistBinding
+import me.wcy.music.utils.ConvertUtils
+import me.wcy.music.utils.ImageUtils.loadCover
+import me.wcy.music.utils.MusicUtils
+import me.wcy.radapter3.RItemBinder
+
+/**
+ *
+ */
+class SearchPlaylistItemBinder(private val onItemClick: (PlaylistData) -> Unit) :
+ RItemBinder() {
+ var keywords = ""
+
+ @SuppressLint("SetTextI18n")
+ override fun onBind(viewBinding: ItemSearchPlaylistBinding, item: PlaylistData, position: Int) {
+ viewBinding.root.setOnClickListener {
+ onItemClick(item)
+ }
+ viewBinding.ivCover.loadCover(item.getSmallCover(), SizeUtils.dp2px(4f))
+ viewBinding.tvTitle.text = MusicUtils.keywordsTint(viewBinding.context, item.name, keywords)
+ viewBinding.tvSubTitle.text = "${item.trackCount}首 , by ${item.creator.nickname} , 播放${
+ ConvertUtils.formatPlayCount(item.playCount, 1)
+ }次"
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/search/song/SearchSongFragment.kt b/AAmusic/app/src/main/java/me/wcy/music/search/song/SearchSongFragment.kt
new file mode 100644
index 0000000..4ea166f
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/search/song/SearchSongFragment.kt
@@ -0,0 +1,100 @@
+package me.wcy.music.search.song
+
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import me.wcy.music.common.OnItemClickListener2
+import me.wcy.music.common.SimpleMusicRefreshFragment
+import me.wcy.music.common.bean.SongData
+import me.wcy.music.common.dialog.songmenu.SongMoreMenuDialog
+import me.wcy.music.common.dialog.songmenu.items.AlbumMenuItem
+import me.wcy.music.common.dialog.songmenu.items.ArtistMenuItem
+import me.wcy.music.common.dialog.songmenu.items.CollectMenuItem
+import me.wcy.music.common.dialog.songmenu.items.CommentMenuItem
+import me.wcy.music.consts.Consts
+import me.wcy.music.consts.RoutePath
+import me.wcy.music.search.SearchApi
+import me.wcy.music.search.SearchViewModel
+import me.wcy.music.service.PlayerController
+import me.wcy.music.utils.toMediaItem
+import me.wcy.radapter3.RAdapter
+import me.wcy.router.CRouter
+import top.wangchenyan.common.model.CommonResult
+import top.wangchenyan.common.net.apiCall
+import javax.inject.Inject
+
+/**
+
+ */
+@AndroidEntryPoint
+class SearchSongFragment : SimpleMusicRefreshFragment() {
+ private val viewModel by activityViewModels()
+ private val itemBinder by lazy {
+ SearchSongItemBinder(object : OnItemClickListener2 {
+ override fun onItemClick(item: SongData, position: Int) {
+ playerController.addAndPlay(item.toMediaItem())
+ CRouter.with(context).url(RoutePath.PLAYING).start()
+ }
+
+ override fun onMoreClick(item: SongData, position: Int) {
+ SongMoreMenuDialog(requireActivity(), item)
+ .setItems(
+ listOf(
+ CollectMenuItem(lifecycleScope, item),
+ CommentMenuItem(item),
+ ArtistMenuItem(item),
+ AlbumMenuItem(item)
+ )
+ )
+ .show()
+ }
+ }).apply {
+ keywords = viewModel.keywords.value
+ }
+ }
+
+ @Inject
+ lateinit var playerController: PlayerController
+
+ override fun isShowTitle(): Boolean {
+ return false
+ }
+
+ override fun isRefreshEnabled(): Boolean {
+ return false
+ }
+
+ override fun onLazyCreate() {
+ super.onLazyCreate()
+ lifecycleScope.launch {
+ viewModel.keywords.collectLatest {
+ if (it.isNotEmpty()) {
+ showLoadSirLoading()
+ itemBinder.keywords = it
+ autoRefresh(true)
+ }
+ }
+ }
+ }
+
+ override fun initAdapter(adapter: RAdapter) {
+ adapter.register(itemBinder)
+ }
+
+ override suspend fun getData(page: Int): CommonResult> {
+ val keywords = viewModel.keywords.value
+ if (keywords.isEmpty()) {
+ return CommonResult.success(emptyList())
+ }
+ val res = apiCall {
+ SearchApi.get().search(1, keywords, Consts.PAGE_COUNT, (page - 1) * Consts.PAGE_COUNT)
+ }
+ return if (res.isSuccessWithData()) {
+ CommonResult.success(res.getDataOrThrow().songs)
+ } else {
+ CommonResult.fail(res.code, res.msg)
+ }
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/search/song/SearchSongItemBinder.kt b/AAmusic/app/src/main/java/me/wcy/music/search/song/SearchSongItemBinder.kt
new file mode 100644
index 0000000..eaf0e74
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/search/song/SearchSongItemBinder.kt
@@ -0,0 +1,39 @@
+package me.wcy.music.search.song
+
+import androidx.core.view.isVisible
+import top.wangchenyan.common.ext.context
+import me.wcy.music.common.OnItemClickListener2
+import me.wcy.music.common.bean.SongData
+import me.wcy.music.databinding.ItemSearchSongBinding
+import me.wcy.music.utils.MusicUtils
+import me.wcy.music.utils.getSimpleArtist
+import me.wcy.radapter3.RItemBinder
+
+/**
+ *
+ */
+class SearchSongItemBinder(private val listener: OnItemClickListener2) :
+ RItemBinder() {
+ var keywords = ""
+
+ override fun onBind(viewBinding: ItemSearchSongBinding, item: SongData, position: Int) {
+ viewBinding.root.setOnClickListener {
+ listener.onItemClick(item, position)
+ }
+ viewBinding.ivMore.setOnClickListener {
+ listener.onMoreClick(item, position)
+ }
+ viewBinding.tvTitle.text = MusicUtils.keywordsTint(viewBinding.context, item.name, keywords)
+ viewBinding.tvTag.isVisible = item.recommendReason.isNotEmpty()
+ viewBinding.tvTag.text = item.recommendReason
+ viewBinding.tvSubTitle.text = buildString {
+ append(item.getSimpleArtist())
+ append(" - ")
+ append(item.al.name)
+ item.originSongSimpleData?.let { originSong ->
+ append(" | 原唱: ")
+ append(originSong.artists.joinToString("/") { it.name })
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/service/MusicService.kt b/AAmusic/app/src/main/java/me/wcy/music/service/MusicService.kt
new file mode 100644
index 0000000..5308712
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/service/MusicService.kt
@@ -0,0 +1,84 @@
+package me.wcy.music.service
+
+import android.app.PendingIntent
+import android.content.Intent
+import androidx.annotation.OptIn
+import androidx.media3.common.AudioAttributes
+import androidx.media3.common.Player
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
+import androidx.media3.session.DefaultMediaNotificationProvider
+import androidx.media3.session.MediaSession
+import androidx.media3.session.MediaSessionService
+import com.blankj.utilcode.util.IntentUtils
+import me.wcy.music.R
+import me.wcy.music.net.datasource.MusicDataSource
+import top.wangchenyan.common.CommonApp
+
+/**
+ *
+ */
+class MusicService : MediaSessionService() {
+ private lateinit var player: Player
+ private lateinit var session: MediaSession
+
+ @OptIn(UnstableApi::class)
+ override fun onCreate() {
+ super.onCreate()
+
+ @OptIn(UnstableApi::class)
+ player = ExoPlayer.Builder(applicationContext)
+ // 自动处理音频焦点
+ .setAudioAttributes(AudioAttributes.DEFAULT, true)
+ // 自动暂停播放
+ .setHandleAudioBecomingNoisy(true)
+ .setMediaSourceFactory(
+ DefaultMediaSourceFactory(applicationContext)
+ .setDataSourceFactory(MusicDataSource.Factory(applicationContext))
+ )
+ .build()
+
+ session = MediaSession.Builder(this, player)
+ .setSessionActivity(
+ PendingIntent.getActivity(
+ this,
+ 0,
+ IntentUtils.getLaunchAppIntent(packageName).apply {
+ putExtra(EXTRA_NOTIFICATION, true)
+ action = Intent.ACTION_VIEW
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ )
+ .build()
+
+ setMediaNotificationProvider(
+ DefaultMediaNotificationProvider.Builder(applicationContext).build().apply {
+ setSmallIcon(R.drawable.ic_notification)
+ }
+ )
+ }
+
+ override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
+ return session
+ }
+
+ override fun onTaskRemoved(rootIntent: Intent?) {
+ super.onTaskRemoved(rootIntent)
+ player.stop()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ player.release()
+ session.release()
+ }
+
+ companion object {
+ val EXTRA_NOTIFICATION = "${CommonApp.app.packageName}.notification"
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/service/PlayMode.kt b/AAmusic/app/src/main/java/me/wcy/music/service/PlayMode.kt
new file mode 100644
index 0000000..5d21e02
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/service/PlayMode.kt
@@ -0,0 +1,25 @@
+package me.wcy.music.service
+
+import androidx.annotation.StringRes
+import me.wcy.music.R
+
+/**
+ * 播放模式
+ * Created by wcy on 2015/12/26.
+ */
+sealed class PlayMode(val value: Int, @StringRes val nameRes: Int) {
+ object Loop : PlayMode(0, R.string.play_mode_loop)
+ object Shuffle : PlayMode(1, R.string.play_mode_shuffle)
+ object Single : PlayMode(2, R.string.play_mode_single)
+
+ companion object {
+ fun valueOf(value: Int): PlayMode {
+ return when (value) {
+ 0 -> Loop
+ 1 -> Shuffle
+ 2 -> Single
+ else -> Loop
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/service/PlayServiceModule.kt b/AAmusic/app/src/main/java/me/wcy/music/service/PlayServiceModule.kt
new file mode 100644
index 0000000..5bb93d3
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/service/PlayServiceModule.kt
@@ -0,0 +1,51 @@
+package me.wcy.music.service
+
+import android.app.Application
+import androidx.lifecycle.MutableLiveData
+import androidx.media3.common.Player
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.EntryPoint
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import me.wcy.music.ext.accessEntryPoint
+import me.wcy.music.storage.db.MusicDatabase
+import top.wangchenyan.common.ext.toUnMutable
+
+/**
+ *
+ */
+@Module
+@InstallIn(SingletonComponent::class)
+object PlayServiceModule {
+ private var player: Player? = null
+ private var playerController: PlayerController? = null
+
+ private val _isPlayerReady = MutableLiveData(false)
+ val isPlayerReady = _isPlayerReady.toUnMutable()
+
+ fun setPlayer(player: Player) {
+ this.player = player
+ _isPlayerReady.value = true
+ }
+
+ @Provides
+ fun providerPlayerController(db: MusicDatabase): PlayerController {
+ return playerController ?: run {
+ val player = player ?: throw IllegalStateException("Player not prepared!")
+ PlayerControllerImpl(player, db).also {
+ playerController = it
+ }
+ }
+ }
+
+ fun Application.playerController(): PlayerController {
+ return accessEntryPoint().playerController()
+ }
+
+ @EntryPoint
+ @InstallIn(SingletonComponent::class)
+ interface PlayerControllerEntryPoint {
+ fun playerController(): PlayerController
+ }
+}
diff --git a/AAmusic/app/src/main/java/me/wcy/music/service/PlayState.kt b/AAmusic/app/src/main/java/me/wcy/music/service/PlayState.kt
new file mode 100644
index 0000000..9c36e5a
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/service/PlayState.kt
@@ -0,0 +1,20 @@
+package me.wcy.music.service
+
+/**
+ *
+ */
+sealed class PlayState {
+ object Idle : PlayState()
+ object Preparing : PlayState()
+ object Playing : PlayState()
+ object Pause : PlayState()
+
+ val isIdle: Boolean
+ get() = this is Idle
+ val isPreparing: Boolean
+ get() = this is Preparing
+ val isPlaying: Boolean
+ get() = this is Playing
+ val isPausing: Boolean
+ get() = this is Pause
+}
diff --git a/AAmusic/app/src/main/java/me/wcy/music/service/PlayerController.kt b/AAmusic/app/src/main/java/me/wcy/music/service/PlayerController.kt
new file mode 100644
index 0000000..58bb4c0
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/service/PlayerController.kt
@@ -0,0 +1,54 @@
+package me.wcy.music.service
+
+import androidx.annotation.MainThread
+import androidx.lifecycle.LiveData
+import androidx.media3.common.MediaItem
+import kotlinx.coroutines.flow.StateFlow
+
+/**
+ *
+ */
+interface PlayerController {
+ val playlist: LiveData>
+ val currentSong: LiveData
+ val playState: StateFlow
+ val playProgress: StateFlow
+ val bufferingPercent: StateFlow
+ val playMode: StateFlow
+
+ @MainThread
+ fun addAndPlay(song: MediaItem)
+
+ @MainThread
+ fun replaceAll(songList: List, song: MediaItem)
+
+ @MainThread
+ fun play(mediaId: String)
+
+ @MainThread
+ fun delete(song: MediaItem)
+
+ @MainThread
+ fun clearPlaylist()
+
+ @MainThread
+ fun playPause()
+
+ @MainThread
+ fun next()
+
+ @MainThread
+ fun prev()
+
+ @MainThread
+ fun seekTo(msec: Int)
+
+ @MainThread
+ fun getAudioSessionId(): Int
+
+ @MainThread
+ fun setPlayMode(mode: PlayMode)
+
+ @MainThread
+ fun stop()
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/service/PlayerControllerImpl.kt b/AAmusic/app/src/main/java/me/wcy/music/service/PlayerControllerImpl.kt
new file mode 100644
index 0000000..0068d5b
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/service/PlayerControllerImpl.kt
@@ -0,0 +1,315 @@
+package me.wcy.music.service
+
+import androidx.annotation.MainThread
+import androidx.annotation.OptIn
+import androidx.lifecycle.MutableLiveData
+import androidx.media3.common.MediaItem
+import androidx.media3.common.PlaybackException
+import androidx.media3.common.Player
+import androidx.media3.common.util.UnstableApi
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import me.wcy.music.storage.db.MusicDatabase
+import me.wcy.music.storage.preference.ConfigPreferences
+import me.wcy.music.utils.toMediaItem
+import me.wcy.music.utils.toSongEntity
+import top.wangchenyan.common.ext.toUnMutable
+import top.wangchenyan.common.ext.toast
+
+/**
+ *
+ */
+class PlayerControllerImpl(
+ private val player: Player,
+ private val db: MusicDatabase,
+) : PlayerController, CoroutineScope by MainScope() {
+
+ private val _playlist = MutableLiveData(emptyList())
+ override val playlist = _playlist.toUnMutable()
+
+ private val _currentSong = MutableLiveData(null)
+ override val currentSong = _currentSong.toUnMutable()
+
+ private val _playState = MutableStateFlow(PlayState.Idle)
+ override val playState = _playState.toUnMutable()
+
+ private val _playProgress = MutableStateFlow(0)
+ override val playProgress = _playProgress.toUnMutable()
+
+ private val _bufferingPercent = MutableStateFlow(0)
+ override val bufferingPercent = _bufferingPercent.toUnMutable()
+
+ private val _playMode = MutableStateFlow(PlayMode.valueOf(ConfigPreferences.playMode))
+ override val playMode: StateFlow = _playMode
+
+ private var audioSessionId = 0
+
+ init {
+ player.playWhenReady = false
+ player.addListener(object : Player.Listener {
+ override fun onPlaybackStateChanged(playbackState: Int) {
+ super.onPlaybackStateChanged(playbackState)
+ when (playbackState) {
+ Player.STATE_IDLE -> {
+ _playState.value = PlayState.Idle
+ _playProgress.value = 0
+ _bufferingPercent.value = 0
+ }
+
+ Player.STATE_BUFFERING -> {
+ _playState.value = PlayState.Preparing
+ }
+
+ Player.STATE_READY -> {
+ player.play()
+ _playState.value = PlayState.Playing
+ }
+
+ Player.STATE_ENDED -> {
+ }
+ }
+ }
+
+ override fun onIsPlayingChanged(isPlaying: Boolean) {
+ super.onIsPlayingChanged(isPlaying)
+ if (player.playbackState == Player.STATE_READY) {
+ _playState.value = if (isPlaying) PlayState.Playing else PlayState.Pause
+ }
+ }
+
+ override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
+ super.onMediaItemTransition(mediaItem, reason)
+ mediaItem ?: return
+ val playlist = _playlist.value ?: return
+ _currentSong.value = playlist.find { it.mediaId == mediaItem.mediaId }
+ }
+
+ @OptIn(UnstableApi::class)
+ override fun onAudioSessionIdChanged(audioSessionId: Int) {
+ super.onAudioSessionIdChanged(audioSessionId)
+ this@PlayerControllerImpl.audioSessionId = audioSessionId
+ }
+
+ override fun onPlayerError(error: PlaybackException) {
+ super.onPlayerError(error)
+ stop()
+ toast("播放失败(${error.errorCodeName},${error.localizedMessage})")
+ }
+ })
+ setPlayMode(PlayMode.valueOf(ConfigPreferences.playMode))
+
+ launch(Dispatchers.Main.immediate) {
+ val playlist = withContext(Dispatchers.IO) {
+ db.playlistDao()
+ .queryAll()
+ .map { it.toMediaItem() }
+ }
+ if (playlist.isNotEmpty()) {
+ _playlist.value = playlist
+ player.setMediaItems(playlist)
+ val currentSongId = ConfigPreferences.currentSongId
+ if (currentSongId.isNotEmpty()) {
+ val currentSongIndex = playlist.indexOfFirst {
+ it.mediaId == currentSongId
+ }.coerceAtLeast(0)
+ _currentSong.value = playlist[currentSongIndex]
+ player.seekTo(currentSongIndex, 0)
+ }
+ }
+
+ _currentSong.observeForever {
+ ConfigPreferences.currentSongId = it?.mediaId ?: ""
+ }
+ }
+
+ launch {
+ while (true) {
+ if (player.isPlaying) {
+ _playProgress.value = player.currentPosition
+ }
+ delay(1000)
+ }
+ }
+ }
+
+ @MainThread
+ override fun addAndPlay(song: MediaItem) {
+ launch(Dispatchers.Main.immediate) {
+ val newPlaylist = _playlist.value?.toMutableList() ?: mutableListOf()
+ val index = newPlaylist.indexOfFirst { it.mediaId == song.mediaId }
+ if (index >= 0) {
+ newPlaylist[index] = song
+ player.replaceMediaItem(index, song)
+ } else {
+ newPlaylist.add(song)
+ player.addMediaItem(song)
+ }
+ withContext(Dispatchers.IO) {
+ db.playlistDao().clear()
+ db.playlistDao().insertAll(newPlaylist.map { it.toSongEntity() })
+ }
+ _playlist.value = newPlaylist
+ play(song.mediaId)
+ }
+ }
+
+ @MainThread
+ override fun replaceAll(songList: List, song: MediaItem) {
+ launch(Dispatchers.Main.immediate) {
+ withContext(Dispatchers.IO) {
+ db.playlistDao().clear()
+ db.playlistDao().insertAll(songList.map { it.toSongEntity() })
+ }
+ stop()
+ player.setMediaItems(songList)
+ _playlist.value = songList
+ _currentSong.value = song
+ play(song.mediaId)
+ }
+ }
+
+ @MainThread
+ override fun play(mediaId: String) {
+ val playlist = _playlist.value
+ if (playlist.isNullOrEmpty()) {
+ return
+ }
+ val index = playlist.indexOfFirst { it.mediaId == mediaId }
+ if (index < 0) {
+ return
+ }
+
+ stop()
+ player.seekTo(index, 0)
+ player.prepare()
+
+ _currentSong.value = playlist[index]
+ _playProgress.value = 0
+ _bufferingPercent.value = 0
+ }
+
+ @MainThread
+ override fun delete(song: MediaItem) {
+ launch(Dispatchers.Main.immediate) {
+ val playlist = _playlist.value?.toMutableList() ?: mutableListOf()
+ val index = playlist.indexOfFirst { it.mediaId == song.mediaId }
+ if (index < 0) return@launch
+ if (playlist.size == 1) {
+ clearPlaylist()
+ } else {
+ playlist.removeAt(index)
+ _playlist.value = playlist
+ withContext(Dispatchers.IO) {
+ db.playlistDao().delete(song.toSongEntity())
+ }
+ player.removeMediaItem(index)
+ }
+ }
+ }
+
+ @MainThread
+ override fun clearPlaylist() {
+ launch(Dispatchers.Main.immediate) {
+ withContext(Dispatchers.IO) {
+ db.playlistDao().clear()
+ }
+ stop()
+ player.clearMediaItems()
+ _playlist.value = emptyList()
+ _currentSong.value = null
+ }
+ }
+
+ @MainThread
+ override fun playPause() {
+ if (player.mediaItemCount == 0) return
+ when (player.playbackState) {
+ Player.STATE_IDLE -> {
+ player.prepare()
+ }
+
+ Player.STATE_BUFFERING -> {
+ stop()
+ }
+
+ Player.STATE_READY -> {
+ if (player.isPlaying) {
+ player.pause()
+ _playState.value = PlayState.Pause
+ } else {
+ player.play()
+ _playState.value = PlayState.Playing
+ }
+ }
+
+ Player.STATE_ENDED -> {
+ player.seekToNextMediaItem()
+ player.prepare()
+ }
+ }
+ }
+
+ @MainThread
+ override fun next() {
+ if (player.mediaItemCount == 0) return
+ player.seekToNextMediaItem()
+ player.prepare()
+ _playProgress.value = 0
+ _bufferingPercent.value = 0
+ }
+
+ @MainThread
+ override fun prev() {
+ if (player.mediaItemCount == 0) return
+ player.seekToPreviousMediaItem()
+ player.prepare()
+ _playProgress.value = 0
+ _bufferingPercent.value = 0
+ }
+
+ @MainThread
+ override fun seekTo(msec: Int) {
+ if (player.playbackState == Player.STATE_READY) {
+ player.seekTo(msec.toLong())
+ }
+ }
+
+ @MainThread
+ override fun getAudioSessionId(): Int {
+ return audioSessionId
+ }
+
+ @MainThread
+ override fun setPlayMode(mode: PlayMode) {
+ ConfigPreferences.playMode = mode.value
+ _playMode.value = mode
+ when (mode) {
+ PlayMode.Loop -> {
+ player.repeatMode = Player.REPEAT_MODE_ALL
+ player.shuffleModeEnabled = false
+ }
+
+ PlayMode.Shuffle -> {
+ player.repeatMode = Player.REPEAT_MODE_ALL
+ player.shuffleModeEnabled = true
+ }
+
+ PlayMode.Single -> {
+ player.repeatMode = Player.REPEAT_MODE_ONE
+ player.shuffleModeEnabled = false
+ }
+ }
+ }
+
+ @MainThread
+ override fun stop() {
+ player.stop()
+ _playState.value = PlayState.Idle
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/service/likesong/LikeSongProcessor.kt b/AAmusic/app/src/main/java/me/wcy/music/service/likesong/LikeSongProcessor.kt
new file mode 100644
index 0000000..01d241f
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/service/likesong/LikeSongProcessor.kt
@@ -0,0 +1,19 @@
+package me.wcy.music.service.likesong
+
+import android.app.Activity
+import top.wangchenyan.common.model.CommonResult
+
+
+/**
+ *
+ */
+interface LikeSongProcessor {
+
+ fun init()
+
+ fun updateLikeSongList()
+
+ fun isLiked(id: Long): Boolean
+
+ suspend fun like(activity: Activity, id: Long): CommonResult
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/service/likesong/LikeSongProcessorImpl.kt b/AAmusic/app/src/main/java/me/wcy/music/service/likesong/LikeSongProcessorImpl.kt
new file mode 100644
index 0000000..0ea62f1
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/service/likesong/LikeSongProcessorImpl.kt
@@ -0,0 +1,87 @@
+package me.wcy.music.service.likesong
+
+import android.app.Activity
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import me.wcy.music.account.service.UserService
+import me.wcy.music.mine.MineApi
+import top.wangchenyan.common.model.CommonResult
+import top.wangchenyan.common.net.apiCall
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ *
+ */
+@Singleton
+class LikeSongProcessorImpl @Inject constructor(
+ private val userService: UserService
+) : LikeSongProcessor, CoroutineScope by MainScope() {
+ private val likeSongSet = mutableSetOf()
+
+ override fun init() {
+ launch {
+ userService.profile.collectLatest {
+ if (it != null) {
+ updateLikeSongList()
+ } else {
+ likeSongSet.clear()
+ }
+ }
+ }
+ }
+
+ override fun updateLikeSongList() {
+ if (userService.isLogin().not()) return
+ launch {
+ val res = runCatching {
+ MineApi.get().getMyLikeSongList(userService.getUserId())
+ }
+ val data = res.getOrNull()
+ if (data?.code == 200) {
+ likeSongSet.clear()
+ likeSongSet.addAll(data.ids)
+ }
+ }
+ }
+
+ override fun isLiked(id: Long): Boolean {
+ if (userService.isLogin().not()) {
+ return false
+ }
+ return likeSongSet.contains(id)
+ }
+
+ override suspend fun like(activity: Activity, id: Long): CommonResult {
+ if (userService.isLogin().not()) {
+ userService.checkLogin(activity)
+ return CommonResult.fail()
+ }
+ val isLike = isLiked(id)
+ if (isLike) {
+ val res = apiCall {
+ MineApi.get().likeSong(id, false)
+ }
+ return if (res.isSuccess()) {
+ likeSongSet.remove(id)
+ updateLikeSongList()
+ CommonResult.success(Unit)
+ } else {
+ CommonResult.fail(res.code, res.msg)
+ }
+ } else {
+ val res = apiCall {
+ MineApi.get().likeSong(id, true)
+ }
+ return if (res.isSuccess()) {
+ likeSongSet.add(id)
+ updateLikeSongList()
+ CommonResult.success(Unit)
+ } else {
+ CommonResult.fail(res.code, res.msg)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/service/likesong/LikeSongProcessorModule.kt b/AAmusic/app/src/main/java/me/wcy/music/service/likesong/LikeSongProcessorModule.kt
new file mode 100644
index 0000000..9bc0c94
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/service/likesong/LikeSongProcessorModule.kt
@@ -0,0 +1,34 @@
+package me.wcy.music.service.likesong
+
+import android.app.Application
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.EntryPoint
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import me.wcy.music.ext.accessEntryPoint
+
+/**
+ *
+ */
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class LikeSongProcessorModule {
+
+ @Binds
+ abstract fun bindLikeSongProcessor(
+ likeSongProcessor: LikeSongProcessorImpl
+ ): LikeSongProcessor
+
+ companion object {
+ fun Application.audioPlayer(): LikeSongProcessor {
+ return accessEntryPoint().likeSongProcessor()
+ }
+ }
+
+ @EntryPoint
+ @InstallIn(SingletonComponent::class)
+ interface LikeSongProcessorEntryPoint {
+ fun likeSongProcessor(): LikeSongProcessor
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/service/likesong/bean/LikeSongListData.kt b/AAmusic/app/src/main/java/me/wcy/music/service/likesong/bean/LikeSongListData.kt
new file mode 100644
index 0000000..5f3df4f
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/service/likesong/bean/LikeSongListData.kt
@@ -0,0 +1,13 @@
+package me.wcy.music.service.likesong.bean
+
+import com.google.gson.annotations.SerializedName
+
+/**
+ *
+ */
+data class LikeSongListData(
+ @SerializedName("code")
+ val code: Int = 0,
+ @SerializedName("ids")
+ val ids: Set = emptySet()
+)
diff --git a/AAmusic/app/src/main/java/me/wcy/music/storage/LrcCache.kt b/AAmusic/app/src/main/java/me/wcy/music/storage/LrcCache.kt
new file mode 100644
index 0000000..d44c7f7
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/storage/LrcCache.kt
@@ -0,0 +1,42 @@
+package me.wcy.music.storage
+
+import androidx.media3.common.MediaItem
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import me.wcy.music.consts.FilePath
+import me.wcy.music.utils.getSongId
+import me.wcy.music.utils.isLocal
+import java.io.File
+
+/**
+ *
+ */
+object LrcCache {
+
+ /**
+ * 获取歌词路径
+ */
+ fun getLrcFilePath(music: MediaItem): String? {
+ if (music.isLocal()) {
+ val audioFile = File(music.localConfiguration?.uri?.toString() ?: "")
+ val lrcFile = File(audioFile.parent, "${audioFile.nameWithoutExtension}.lrc")
+ if (lrcFile.exists()) {
+ return lrcFile.path
+ }
+ } else {
+ val lrcFile = File(FilePath.lrcDir, music.getSongId().toString())
+ if (lrcFile.exists()) {
+ return lrcFile.path
+ }
+ }
+ return null
+ }
+
+ suspend fun saveLrcFile(music: MediaItem, content: String): File {
+ return withContext(Dispatchers.IO) {
+ File(FilePath.lrcDir, music.getSongId().toString()).also {
+ it.writeText(content)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/storage/db/DatabaseModule.kt b/AAmusic/app/src/main/java/me/wcy/music/storage/db/DatabaseModule.kt
new file mode 100644
index 0000000..882c421
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/storage/db/DatabaseModule.kt
@@ -0,0 +1,25 @@
+package me.wcy.music.storage.db
+
+import androidx.room.Room
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import top.wangchenyan.common.CommonApp
+
+/**
+ *
+ */
+@Module
+@InstallIn(SingletonComponent::class)
+object DatabaseModule {
+
+ @Provides
+ fun provideAppDatabase(): MusicDatabase {
+ return Room.databaseBuilder(
+ CommonApp.app,
+ MusicDatabase::class.java,
+ "music_db"
+ ).build()
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/storage/db/MusicDatabase.kt b/AAmusic/app/src/main/java/me/wcy/music/storage/db/MusicDatabase.kt
new file mode 100644
index 0000000..5badb27
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/storage/db/MusicDatabase.kt
@@ -0,0 +1,20 @@
+package me.wcy.music.storage.db
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import me.wcy.music.storage.db.dao.PlaylistDao
+import me.wcy.music.storage.db.entity.SongEntity
+
+/**
+ *
+ */
+@Database(
+ entities = [
+ SongEntity::class,
+ ],
+ version = 1
+)
+abstract class MusicDatabase : RoomDatabase() {
+
+ abstract fun playlistDao(): PlaylistDao
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/storage/db/dao/PlaylistDao.kt b/AAmusic/app/src/main/java/me/wcy/music/storage/db/dao/PlaylistDao.kt
new file mode 100644
index 0000000..611fee1
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/storage/db/dao/PlaylistDao.kt
@@ -0,0 +1,32 @@
+package me.wcy.music.storage.db.dao
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import me.wcy.music.storage.db.entity.SongEntity
+
+/**
+ *
+ */
+@Dao
+interface PlaylistDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(entity: SongEntity)
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertAll(list: List)
+
+ @Query("SELECT * FROM play_list")
+ fun queryAll(): List
+
+ @Query("SELECT * FROM play_list WHERE unique_id = :uniqueId")
+ fun queryByUniqueId(uniqueId: String): SongEntity?
+
+ @Delete
+ fun delete(entity: SongEntity)
+
+ @Query("DELETE FROM play_list")
+ fun clear()
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/storage/db/entity/SongEntity.kt b/AAmusic/app/src/main/java/me/wcy/music/storage/db/entity/SongEntity.kt
new file mode 100644
index 0000000..3d07b28
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/storage/db/entity/SongEntity.kt
@@ -0,0 +1,97 @@
+package me.wcy.music.storage.db.entity
+
+import android.os.Parcelable
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.Index
+import androidx.room.PrimaryKey
+import kotlinx.parcelize.Parcelize
+import me.wcy.music.utils.MusicUtils.asLargeCover
+import me.wcy.music.utils.MusicUtils.asSmallCover
+import me.wcy.music.utils.generateUniqueId
+
+/**
+ *
+ */
+@Parcelize
+@Entity("play_list", indices = [Index("title"), Index("artist"), Index("album")])
+data class SongEntity(
+ // 歌曲类型:本地/网络
+ @ColumnInfo("type")
+ val type: Int = 0,
+
+ // 歌曲ID
+ @ColumnInfo("song_id")
+ val songId: Long = 0,
+
+ // 音乐标题
+ @ColumnInfo("title")
+ val title: String = "",
+
+ // 艺术家
+ @ColumnInfo("artist")
+ val artist: String = "",
+
+ // 艺术家ID
+ @ColumnInfo("artist_id")
+ val artistId: Long = 0,
+
+ // 专辑
+ @ColumnInfo("album")
+ val album: String = "",
+
+ // 专辑ID
+ @ColumnInfo("album_id")
+ val albumId: Long = 0,
+
+ // 专辑封面
+ @Deprecated("Please use resized url")
+ @ColumnInfo("album_cover")
+ val albumCover: String = "",
+
+ // 持续时间
+ @ColumnInfo("duration")
+ val duration: Long = 0,
+
+ // 播放地址
+ @ColumnInfo("path")
+ var path: String = "",
+
+ // [本地]文件名
+ @ColumnInfo("file_name")
+ val fileName: String = "",
+
+ // [本地]文件大小
+ @ColumnInfo("file_size")
+ val fileSize: Long = 0,
+) : Parcelable {
+ @PrimaryKey
+ @ColumnInfo("unique_id")
+ var uniqueId: String = generateUniqueId(type, songId)
+
+ override fun hashCode(): Int {
+ return uniqueId.hashCode()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is SongEntity
+ && other.uniqueId == this.uniqueId
+ }
+
+ fun isLocal() = type == LOCAL
+
+ fun getSmallCover(): String {
+ if (isLocal()) return albumCover
+ return albumCover.asSmallCover()
+ }
+
+ fun getLargeCover(): String {
+ if (isLocal()) return albumCover
+ return albumCover.asLargeCover()
+ }
+
+ companion object {
+ const val LOCAL = 0
+ const val ONLINE = 1
+ }
+}
diff --git a/AAmusic/app/src/main/java/me/wcy/music/storage/preference/ConfigPreferences.kt b/AAmusic/app/src/main/java/me/wcy/music/storage/preference/ConfigPreferences.kt
new file mode 100644
index 0000000..0aac11d
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/storage/preference/ConfigPreferences.kt
@@ -0,0 +1,48 @@
+package me.wcy.music.storage.preference
+
+import com.blankj.utilcode.util.StringUtils
+import me.wcy.music.R
+import me.wcy.music.common.DarkModeService
+import me.wcy.music.consts.PreferenceName
+import top.wangchenyan.common.CommonApp
+import top.wangchenyan.common.storage.IPreferencesFile
+import top.wangchenyan.common.storage.PreferencesFile
+
+/**
+ * SharedPreferences工具类
+ * Created by wcy on 2015/11/28.
+ */
+object ConfigPreferences :
+ IPreferencesFile by PreferencesFile(CommonApp.app, PreferenceName.CONFIG, false) {
+
+ var playSoundQuality by IPreferencesFile.StringProperty(
+ StringUtils.getString(R.string.setting_key_play_sound_quality),
+ "standard"
+ )
+
+ var downloadSoundQuality by IPreferencesFile.StringProperty(
+ StringUtils.getString(R.string.setting_key_download_sound_quality),
+ "standard"
+ )
+
+ var filterSize by IPreferencesFile.StringProperty(
+ StringUtils.getString(R.string.setting_key_filter_size),
+ "0"
+ )
+
+ var filterTime by IPreferencesFile.StringProperty(
+ StringUtils.getString(R.string.setting_key_filter_time),
+ "0"
+ )
+
+ var darkMode by IPreferencesFile.StringProperty(
+ "dark_mode",
+ DarkModeService.DarkMode.Auto.value
+ )
+
+ var playMode: Int by IPreferencesFile.IntProperty("play_mode", 0)
+
+ var currentSongId: String by IPreferencesFile.StringProperty("current_song_id", "")
+
+ var apiDomain: String by IPreferencesFile.StringProperty("api_domain", "")
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/utils/ConvertUtils.kt b/AAmusic/app/src/main/java/me/wcy/music/utils/ConvertUtils.kt
new file mode 100644
index 0000000..f947c7f
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/utils/ConvertUtils.kt
@@ -0,0 +1,23 @@
+package me.wcy.music.utils
+
+import top.wangchenyan.common.ext.divide
+import top.wangchenyan.common.ext.format
+import java.math.RoundingMode
+
+/**
+ *
+ */
+object ConvertUtils {
+
+ fun formatPlayCount(num: Long, dot: Int = 0): String {
+ return if (num < 100000) {
+ num.toString()
+ } else if (num < 100000000) {
+ val wan = num.toDouble().divide(10000.0).format(dot, RoundingMode.HALF_DOWN)
+ wan + "万"
+ } else {
+ val wan = num.toDouble().divide(100000000.0).format(dot, RoundingMode.HALF_DOWN)
+ wan + "亿"
+ }
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/utils/ImageUtils.kt b/AAmusic/app/src/main/java/me/wcy/music/utils/ImageUtils.kt
new file mode 100644
index 0000000..ff64f55
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/utils/ImageUtils.kt
@@ -0,0 +1,38 @@
+package me.wcy.music.utils
+
+import android.graphics.Bitmap
+import android.widget.ImageView
+import com.bumptech.glide.load.resource.bitmap.CenterCrop
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners
+import top.wangchenyan.common.ext.load
+import me.wcy.music.R
+
+/**
+ * 图像工具类
+ * Created by wcy on 2015/11/29.
+ */
+object ImageUtils {
+
+ /**
+ * 将图片放大或缩小到指定尺寸
+ */
+ fun resizeImage(source: Bitmap, dstWidth: Int, dstHeight: Int): Bitmap {
+ return if (source.width == dstWidth && source.height == dstHeight) {
+ source
+ } else {
+ Bitmap.createScaledBitmap(source, dstWidth, dstHeight, true)
+ }
+ }
+
+ fun ImageView.loadCover(url: Any?, corners: Int) {
+ load(url) {
+ placeholder(R.drawable.ic_default_cover)
+ error(R.drawable.ic_default_cover)
+
+ if (corners > 0) {
+ // 圆角和 CenterCrop 不兼容,需同时设置
+ transform(CenterCrop(), RoundedCorners(corners))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/utils/ModelEx.kt b/AAmusic/app/src/main/java/me/wcy/music/utils/ModelEx.kt
new file mode 100644
index 0000000..ecf64e0
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/utils/ModelEx.kt
@@ -0,0 +1,140 @@
+package me.wcy.music.utils
+
+import android.net.Uri
+import androidx.core.os.bundleOf
+import androidx.media3.common.MediaItem
+import androidx.media3.common.MediaMetadata
+import me.wcy.music.common.bean.SongData
+import me.wcy.music.storage.db.entity.SongEntity
+import top.wangchenyan.common.CommonApp
+
+/**
+ *
+ */
+
+const val SCHEME_NETEASE = "netease"
+const val PARAM_ID = "id"
+const val EXTRA_DURATION = "duration"
+const val EXTRA_FILE_NAME = "file_name"
+const val EXTRA_FILE_SIZE = "file_size"
+const val EXTRA_SMALL_COVER = "small_cover"
+
+fun SongData.getSimpleArtist(): String {
+ return ar.joinToString("/") { it.name }
+}
+
+fun SongEntity.toMediaItem(): MediaItem {
+ return MediaItem.Builder()
+ .setMediaId(uniqueId)
+ .setUri(path)
+ .setMediaMetadata(
+ MediaMetadata.Builder()
+ .setTitle(title)
+ .setArtist(artist)
+ .setAlbumTitle(album)
+ .setAlbumArtist(artist)
+ .setArtworkUri(Uri.parse(getLargeCover()))
+ .setSmallCover(getSmallCover())
+ .setDuration(duration)
+ .setFileName(fileName)
+ .setFileSize(fileSize)
+ .build()
+ )
+ .build()
+}
+
+fun MediaItem.toSongEntity(): SongEntity {
+ return SongEntity(
+ type = getSongType(),
+ songId = getSongId(),
+ title = mediaMetadata.title?.toString() ?: "",
+ artist = mediaMetadata.artist?.toString() ?: "",
+ artistId = 0,
+ album = mediaMetadata.albumTitle?.toString() ?: "",
+ albumId = 0,
+ albumCover = mediaMetadata.artworkUri?.toString() ?: "",
+ duration = mediaMetadata.getDuration(),
+ path = localConfiguration?.uri?.toString() ?: "",
+ fileName = mediaMetadata.getFileName(),
+ fileSize = mediaMetadata.getFileSize()
+ )
+}
+
+fun SongData.toMediaItem(): MediaItem {
+ val uri = Uri.Builder()
+ .scheme(SCHEME_NETEASE)
+ .authority(CommonApp.app.packageName)
+ .appendQueryParameter(PARAM_ID, id.toString())
+ .build()
+ return MediaItem.Builder()
+ .setMediaId(generateUniqueId(SongEntity.ONLINE, id))
+ .setUri(uri)
+ .setMediaMetadata(
+ MediaMetadata.Builder()
+ .setTitle(name)
+ .setArtist(getSimpleArtist())
+ .setAlbumTitle(al.name)
+ .setAlbumArtist(getSimpleArtist())
+ .setArtworkUri(Uri.parse(al.getLargeCover()))
+ .setSmallCover(al.getSmallCover())
+ .setDuration(dt)
+ .build()
+ )
+ .build()
+}
+
+fun generateUniqueId(type: Int, songId: Long): String {
+ return "$type#$songId"
+}
+
+fun MediaItem.isLocal(): Boolean {
+ return getSongType() == SongEntity.LOCAL
+}
+
+fun MediaItem.getSongType(): Int {
+ return mediaId.split("#").firstOrNull()?.toIntOrNull() ?: SongEntity.LOCAL
+}
+
+fun MediaItem.getSongId(): Long {
+ return mediaId.split("#").getOrNull(1)?.toLongOrNull() ?: 0L
+}
+
+fun MediaMetadata.Builder.setDuration(duration: Long) = apply {
+ val extras = build().extras ?: bundleOf()
+ extras.putLong(EXTRA_DURATION, duration)
+ setExtras(extras)
+}
+
+fun MediaMetadata.getDuration(): Long {
+ return extras?.getLong(EXTRA_DURATION) ?: 0
+}
+
+fun MediaMetadata.Builder.setFileName(name: String) = apply {
+ val extras = build().extras ?: bundleOf()
+ extras.putString(EXTRA_FILE_NAME, name)
+ setExtras(extras)
+}
+
+fun MediaMetadata.getFileName(): String {
+ return extras?.getString(EXTRA_FILE_NAME) ?: ""
+}
+
+fun MediaMetadata.Builder.setFileSize(size: Long) = apply {
+ val extras = build().extras ?: bundleOf()
+ extras.putLong(EXTRA_FILE_SIZE, size)
+ setExtras(extras)
+}
+
+fun MediaMetadata.getFileSize(): Long {
+ return extras?.getLong(EXTRA_FILE_SIZE) ?: 0
+}
+
+fun MediaMetadata.Builder.setSmallCover(value: String) = apply {
+ val extras = build().extras ?: bundleOf()
+ extras.putString(EXTRA_SMALL_COVER, value)
+ setExtras(extras)
+}
+
+fun MediaMetadata.getSmallCover(): String {
+ return extras?.getString(EXTRA_SMALL_COVER) ?: ""
+}
diff --git a/AAmusic/app/src/main/java/me/wcy/music/utils/MusicUtils.kt b/AAmusic/app/src/main/java/me/wcy/music/utils/MusicUtils.kt
new file mode 100644
index 0000000..04e2ae9
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/utils/MusicUtils.kt
@@ -0,0 +1,78 @@
+package me.wcy.music.utils
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.media.audiofx.AudioEffect
+import android.text.TextUtils
+import androidx.core.text.buildSpannedString
+import top.wangchenyan.common.ext.getColorEx
+import top.wangchenyan.common.widget.CustomSpan.appendStyle
+import me.wcy.music.R
+
+/**
+ * 歌曲工具类
+ * Created by wcy on 2015/11/27.
+ */
+object MusicUtils {
+
+ fun isAudioControlPanelAvailable(context: Context): Boolean {
+ return isIntentAvailable(
+ context,
+ Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL)
+ )
+ }
+
+ private fun isIntentAvailable(context: Context, intent: Intent): Boolean {
+ return context.packageManager.resolveActivity(
+ intent,
+ PackageManager.GET_RESOLVED_FILTER
+ ) != null
+ }
+
+ fun getArtistAndAlbum(artist: String?, album: String?): String? {
+ return if (TextUtils.isEmpty(artist) && TextUtils.isEmpty(album)) {
+ ""
+ } else if (!TextUtils.isEmpty(artist) && TextUtils.isEmpty(album)) {
+ artist
+ } else if (TextUtils.isEmpty(artist) && !TextUtils.isEmpty(album)) {
+ album
+ } else {
+ "$artist - $album"
+ }
+ }
+
+ fun keywordsTint(context: Context, text: String, keywords: String): CharSequence {
+ if (text.isEmpty() || keywords.isEmpty()) {
+ return text
+ }
+ val splitText = text.split(keywords)
+ return buildSpannedString {
+ splitText.forEachIndexed { index, s ->
+ append(s)
+ if (index < splitText.size - 1) {
+ appendStyle(
+ keywords,
+ color = context.getColorEx(R.color.common_theme_color)
+ )
+ }
+ }
+ }
+ }
+
+ fun String.asSmallCover(): String {
+ return appendImageSize(200)
+ }
+
+ fun String.asLargeCover(): String {
+ return appendImageSize(800)
+ }
+
+ private fun String.appendImageSize(size: Int): String {
+ return if (contains("?")) {
+ "$this¶m=${size}y${size}"
+ } else {
+ "$this?param=${size}y${size}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/utils/QuitTimer.kt b/AAmusic/app/src/main/java/me/wcy/music/utils/QuitTimer.kt
new file mode 100644
index 0000000..357545f
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/utils/QuitTimer.kt
@@ -0,0 +1,51 @@
+package me.wcy.music.utils
+
+import android.os.Handler
+import android.os.Looper
+import android.text.format.DateUtils
+
+/**
+ * Created by hzwangchenyan on 2017/8/8.
+ */
+class QuitTimer(private val listener: OnTimerListener) {
+ private val handler: Handler by lazy {
+ Handler(Looper.getMainLooper())
+ }
+ private var timerRemain: Long = 0
+
+ fun start(milli: Long) {
+ stop()
+ if (milli > 0) {
+ timerRemain = milli + DateUtils.SECOND_IN_MILLIS
+ handler.post(mQuitRunnable)
+ } else {
+ timerRemain = 0
+ listener.onTick(timerRemain)
+ }
+ }
+
+ fun stop() {
+ handler.removeCallbacks(mQuitRunnable)
+ }
+
+ private val mQuitRunnable: Runnable = object : Runnable {
+ override fun run() {
+ timerRemain -= DateUtils.SECOND_IN_MILLIS
+ if (timerRemain > 0) {
+ listener.onTick(timerRemain)
+ handler.postDelayed(this, DateUtils.SECOND_IN_MILLIS)
+ } else {
+ listener.onTimeEnd()
+ }
+ }
+ }
+
+ interface OnTimerListener {
+ /**
+ * 更新定时停止播放时间
+ */
+ fun onTick(remain: Long)
+
+ fun onTimeEnd()
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/utils/TimeUtils.kt b/AAmusic/app/src/main/java/me/wcy/music/utils/TimeUtils.kt
new file mode 100644
index 0000000..bad84d6
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/utils/TimeUtils.kt
@@ -0,0 +1,21 @@
+package me.wcy.music.utils
+
+import android.text.format.DateUtils
+import java.util.Locale
+
+/**
+ * Created by hzwangchenyan on 2016/3/22.
+ */
+object TimeUtils {
+ fun formatMs(milli: Long): String {
+ return formatTime("mm:ss", milli)
+ }
+
+ fun formatTime(pattern: String, milli: Long): String {
+ val m = (milli / DateUtils.MINUTE_IN_MILLIS).toInt()
+ val s = (milli / DateUtils.SECOND_IN_MILLIS % 60).toInt()
+ val mm = String.format(Locale.getDefault(), "%02d", m)
+ val ss = String.format(Locale.getDefault(), "%02d", s)
+ return pattern.replace("mm", mm).replace("ss", ss)
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/widget/AlbumCoverView.kt b/AAmusic/app/src/main/java/me/wcy/music/widget/AlbumCoverView.kt
new file mode 100644
index 0000000..ca3e934
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/widget/AlbumCoverView.kt
@@ -0,0 +1,204 @@
+package me.wcy.music.widget
+
+import android.animation.ValueAnimator
+import android.animation.ValueAnimator.AnimatorUpdateListener
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
+import android.graphics.Matrix
+import android.graphics.Point
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.View
+import android.view.animation.LinearInterpolator
+import androidx.core.content.res.ResourcesCompat
+import com.blankj.utilcode.util.SizeUtils
+import me.wcy.music.R
+import me.wcy.music.utils.ImageUtils
+import top.wangchenyan.common.ext.startOrResume
+
+/**
+ * 专辑封面
+ * Created by wcy on 2015/11/30.
+ */
+class AlbumCoverView @JvmOverloads constructor(
+ context: Context?,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : View(context, attrs, defStyleAttr) {
+ private val coverBorder: Drawable by lazy {
+ ResourcesCompat.getDrawable(resources, R.drawable.bg_playing_cover_border, null)!!
+ }
+
+ private var discBitmap = BitmapFactory.decodeResource(resources, R.drawable.bg_playing_disc)
+ private val discMatrix by lazy { Matrix() }
+ private val discStartPoint by lazy { Point() } // 图片起始坐标
+ private val discCenterPoint by lazy { Point() } // 旋转中心坐标
+ private var discRotation = 0.0f
+
+ private var needleBitmap =
+ BitmapFactory.decodeResource(resources, R.drawable.ic_playing_needle)
+ private val needleMatrix by lazy { Matrix() }
+ private val needleStartPoint by lazy { Point() }
+ private val needleCenterPoint by lazy { Point() }
+ private var needleRotation = NEEDLE_ROTATION_PLAY
+
+ private var coverBitmap: Bitmap? = null
+ private val coverMatrix by lazy { Matrix() }
+ private val coverStartPoint by lazy { Point() }
+ private val coverCenterPoint by lazy { Point() }
+ private var coverSize = 0
+
+ private val rotationAnimator by lazy {
+ ValueAnimator.ofFloat(0f, 360f).apply {
+ duration = 20000
+ repeatCount = ValueAnimator.INFINITE
+ interpolator = LinearInterpolator()
+ addUpdateListener(rotationUpdateListener)
+ }
+ }
+ private val playAnimator by lazy {
+ ValueAnimator.ofFloat(NEEDLE_ROTATION_PAUSE, NEEDLE_ROTATION_PLAY).apply {
+ duration = 300
+ addUpdateListener(animationUpdateListener)
+ }
+ }
+ private val pauseAnimator by lazy {
+ ValueAnimator.ofFloat(NEEDLE_ROTATION_PLAY, NEEDLE_ROTATION_PAUSE).apply {
+ duration = 300
+ addUpdateListener(animationUpdateListener)
+ }
+ }
+
+ private var isPlaying = false
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ if (w > 0 && h > 0) {
+ initSize()
+ }
+ }
+
+ private fun initSize() {
+ val unit = width.coerceAtMost(height) / 8
+
+ needleBitmap = ImageUtils.resizeImage(needleBitmap, unit * 2, (unit * 3.33).toInt())
+ needleStartPoint.x = (width / 2 - needleBitmap.width / 5.5f).toInt()
+ needleStartPoint.y = 0
+ needleCenterPoint.x = width / 2
+ needleCenterPoint.y = (needleBitmap.width / 5.5f).toInt()
+
+ discBitmap = ImageUtils.resizeImage(discBitmap, unit * 6, unit * 6)
+ val discOffsetY = (needleBitmap.height / 1.5).toInt()
+ discStartPoint.x = (width - discBitmap.width) / 2
+ discStartPoint.y = discOffsetY
+ discCenterPoint.x = width / 2
+ discCenterPoint.y = discBitmap.height / 2 + discOffsetY
+
+ coverSize = unit * 4
+ coverStartPoint.x = (width - coverSize) / 2
+ coverStartPoint.y = discOffsetY + (discBitmap.height - coverSize) / 2
+ coverCenterPoint.x = discCenterPoint.x
+ coverCenterPoint.y = discCenterPoint.y
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ // 1.绘制封面
+ val cover = coverBitmap
+ if (cover != null) {
+ coverMatrix.setRotate(
+ discRotation,
+ coverCenterPoint.x.toFloat(),
+ coverCenterPoint.y.toFloat()
+ )
+ coverMatrix.preTranslate(coverStartPoint.x.toFloat(), coverStartPoint.y.toFloat())
+ coverMatrix.preScale(
+ coverSize.toFloat() / cover.width,
+ coverSize.toFloat() / cover.height
+ )
+ canvas.drawBitmap(cover, coverMatrix, null)
+ }
+
+ // 2.绘制黑胶唱片外侧半透明边框
+ coverBorder.setBounds(
+ discStartPoint.x - COVER_BORDER_WIDTH,
+ discStartPoint.y - COVER_BORDER_WIDTH,
+ discStartPoint.x + discBitmap.width + COVER_BORDER_WIDTH,
+ discStartPoint.y + discBitmap.height + COVER_BORDER_WIDTH
+ )
+ coverBorder.draw(canvas)
+
+ // 3.绘制黑胶
+ // 设置旋转中心和旋转角度,setRotate和preTranslate顺序很重要
+ discMatrix.setRotate(
+ discRotation,
+ discCenterPoint.x.toFloat(),
+ discCenterPoint.y.toFloat()
+ )
+ // 设置图片起始坐标
+ discMatrix.preTranslate(discStartPoint.x.toFloat(), discStartPoint.y.toFloat())
+ canvas.drawBitmap(discBitmap, discMatrix, null)
+
+ // 4.绘制指针
+ needleMatrix.setRotate(
+ needleRotation,
+ needleCenterPoint.x.toFloat(),
+ needleCenterPoint.y.toFloat()
+ )
+ needleMatrix.preTranslate(needleStartPoint.x.toFloat(), needleStartPoint.y.toFloat())
+ canvas.drawBitmap(needleBitmap, needleMatrix, null)
+ }
+
+ fun initNeedle(isPlaying: Boolean) {
+ needleRotation = if (isPlaying) NEEDLE_ROTATION_PLAY else NEEDLE_ROTATION_PAUSE
+ invalidate()
+ }
+
+ fun setCoverBitmap(bitmap: Bitmap) {
+ coverBitmap = bitmap
+ invalidate()
+ }
+
+ fun start() {
+ if (isPlaying) {
+ return
+ }
+ isPlaying = true
+ rotationAnimator.startOrResume()
+ playAnimator.start()
+ }
+
+ fun pause() {
+ if (!isPlaying) {
+ return
+ }
+ isPlaying = false
+ rotationAnimator.pause()
+ pauseAnimator.start()
+ }
+
+ fun reset() {
+ isPlaying = false
+ discRotation = 0.0f
+ rotationAnimator.cancel()
+ invalidate()
+ }
+
+ private val rotationUpdateListener = AnimatorUpdateListener { animation ->
+ discRotation = animation.animatedValue as Float
+ invalidate()
+ }
+
+ private val animationUpdateListener = AnimatorUpdateListener { animation ->
+ needleRotation = animation.animatedValue as Float
+ invalidate()
+ }
+
+ companion object {
+ private const val NEEDLE_ROTATION_PLAY = 0.0f
+ private const val NEEDLE_ROTATION_PAUSE = -25.0f
+
+ private val COVER_BORDER_WIDTH = SizeUtils.dp2px(6f)
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/widget/MaxHeightLinearLayout.kt b/AAmusic/app/src/main/java/me/wcy/music/widget/MaxHeightLinearLayout.kt
new file mode 100644
index 0000000..c3123f2
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/widget/MaxHeightLinearLayout.kt
@@ -0,0 +1,39 @@
+package me.wcy.music.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.LinearLayout
+import me.wcy.music.R
+
+/**
+ *
+ */
+class MaxHeightLinearLayout @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : LinearLayout(context, attrs, defStyleAttr) {
+ private var maxHeight: Int = 0
+
+ init {
+ val ta = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightLinearLayout)
+ maxHeight = ta.getDimensionPixelSize(R.styleable.MaxHeightLinearLayout_maxHeight, 0)
+ ta.recycle()
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ if (maxHeight <= 0) {
+ return super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ }
+ val maxHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST)
+ super.onMeasure(widthMeasureSpec, maxHeightMeasureSpec)
+ }
+
+ /**
+ * 设置最大高度,单位为px
+ */
+ fun setMaxHeight(maxHeight: Int) {
+ this.maxHeight = maxHeight
+ requestLayout()
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/widget/PlayBar.kt b/AAmusic/app/src/main/java/me/wcy/music/widget/PlayBar.kt
new file mode 100644
index 0000000..7f70a8c
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/widget/PlayBar.kt
@@ -0,0 +1,123 @@
+package me.wcy.music.widget
+
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.animation.LinearInterpolator
+import android.widget.FrameLayout
+import androidx.core.text.buildSpannedString
+import androidx.core.view.isVisible
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import me.wcy.music.R
+import me.wcy.music.consts.RoutePath
+import me.wcy.music.databinding.LayoutPlayBarBinding
+import me.wcy.music.main.playlist.CurrentPlaylistFragment
+import me.wcy.music.service.PlayServiceModule.playerController
+import me.wcy.music.utils.getDuration
+import me.wcy.music.utils.getSmallCover
+import me.wcy.router.CRouter
+import top.wangchenyan.common.CommonApp
+import top.wangchenyan.common.ext.findActivity
+import top.wangchenyan.common.ext.findLifecycleOwner
+import top.wangchenyan.common.ext.getColor
+import top.wangchenyan.common.ext.loadAvatar
+import top.wangchenyan.common.widget.CustomSpan.appendStyle
+
+/**
+ *
+ */
+class PlayBar @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null
+) : FrameLayout(context, attrs) {
+ private val viewBinding: LayoutPlayBarBinding
+ private val playerController by lazy {
+ CommonApp.app.playerController()
+ }
+ private val rotateAnimator: ObjectAnimator
+
+ init {
+ id = R.id.play_bar
+ viewBinding = LayoutPlayBarBinding.inflate(LayoutInflater.from(context), this, true)
+
+ rotateAnimator = ObjectAnimator.ofFloat(viewBinding.ivCover, "rotation", 0f, 360f).apply {
+ duration = 20000
+ repeatCount = ValueAnimator.INFINITE
+ repeatMode = ObjectAnimator.RESTART
+ interpolator = LinearInterpolator()
+ }
+
+ initView()
+ context.findLifecycleOwner()?.let {
+ initData(it)
+ }
+ }
+
+ private fun initView() {
+ viewBinding.root.setOnClickListener {
+ CRouter.with(context).url(RoutePath.PLAYING).start()
+ }
+ viewBinding.ivPlay.setOnClickListener {
+ playerController.playPause()
+ }
+ viewBinding.ivNext.setOnClickListener {
+ playerController.next()
+ }
+ viewBinding.ivPlaylist.setOnClickListener {
+ val activity = context.findActivity()
+ if (activity is FragmentActivity) {
+ CurrentPlaylistFragment.newInstance()
+ .show(activity.supportFragmentManager, CurrentPlaylistFragment.TAG)
+ }
+ }
+ }
+
+ private fun initData(lifecycleOwner: LifecycleOwner) {
+ playerController.currentSong.observe(lifecycleOwner) { currentSong ->
+ if (currentSong != null) {
+ isVisible = true
+ viewBinding.ivCover.loadAvatar(currentSong.mediaMetadata.getSmallCover())
+ viewBinding.tvTitle.text = buildSpannedString {
+ append(currentSong.mediaMetadata.title)
+ appendStyle(
+ " - ${currentSong.mediaMetadata.artist}",
+ color = getColor(R.color.common_text_h2_color)
+ )
+ }
+ viewBinding.progressBar.max = currentSong.mediaMetadata.getDuration().toInt()
+ viewBinding.progressBar.progress = playerController.playProgress.value.toInt()
+ } else {
+ isVisible = false
+ }
+ }
+
+ lifecycleOwner.lifecycleScope.launch {
+ playerController.playState.collectLatest { playState ->
+ val isPlaying = playState.isPreparing || playState.isPlaying
+ viewBinding.ivPlay.isSelected = isPlaying
+ if (isPlaying) {
+ if (rotateAnimator.isPaused) {
+ rotateAnimator.resume()
+ } else if (rotateAnimator.isStarted.not()) {
+ rotateAnimator.start()
+ }
+ } else {
+ if (rotateAnimator.isRunning) {
+ rotateAnimator.pause()
+ }
+ }
+ }
+ }
+
+ lifecycleOwner.lifecycleScope.launch {
+ playerController.playProgress.collectLatest {
+ viewBinding.progressBar.progress = it.toInt()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/java/me/wcy/music/widget/loadsir/SoundWaveLoadingCallback.kt b/AAmusic/app/src/main/java/me/wcy/music/widget/loadsir/SoundWaveLoadingCallback.kt
new file mode 100644
index 0000000..e13b708
--- /dev/null
+++ b/AAmusic/app/src/main/java/me/wcy/music/widget/loadsir/SoundWaveLoadingCallback.kt
@@ -0,0 +1,19 @@
+package me.wcy.music.widget.loadsir
+
+import android.content.Context
+import android.view.View
+import com.kingja.loadsir.callback.Callback
+import me.wcy.music.R
+
+/**
+ *
+ */
+class SoundWaveLoadingCallback : Callback() {
+ override fun onCreateView(): Int {
+ return R.layout.load_sir_loading_sound_wave
+ }
+
+ override fun onReloadEvent(context: Context?, view: View?): Boolean {
+ return true
+ }
+}
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/anim/anim_slide_down.xml b/AAmusic/app/src/main/res/anim/anim_slide_down.xml
new file mode 100644
index 0000000..88650fb
--- /dev/null
+++ b/AAmusic/app/src/main/res/anim/anim_slide_down.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/AAmusic/app/src/main/res/anim/anim_slide_up.xml b/AAmusic/app/src/main/res/anim/anim_slide_up.xml
new file mode 100644
index 0000000..238dec8
--- /dev/null
+++ b/AAmusic/app/src/main/res/anim/anim_slide_up.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/AAmusic/app/src/main/res/color/color_main_tab_text_selector.xml b/AAmusic/app/src/main/res/color/color_main_tab_text_selector.xml
new file mode 100644
index 0000000..e4e45fa
--- /dev/null
+++ b/AAmusic/app/src/main/res/color/color_main_tab_text_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/color/color_theme_grey_300.xml b/AAmusic/app/src/main/res/color/color_theme_grey_300.xml
new file mode 100644
index 0000000..2812348
--- /dev/null
+++ b/AAmusic/app/src/main/res/color/color_theme_grey_300.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/color/color_theme_h2.xml b/AAmusic/app/src/main/res/color/color_theme_h2.xml
new file mode 100644
index 0000000..0c89480
--- /dev/null
+++ b/AAmusic/app/src/main/res/color/color_theme_h2.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/drawable-anydpi-v26/ic_launcher.xml b/AAmusic/app/src/main/res/drawable-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..7353dbd
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/drawable-anydpi-v26/ic_launcher_round.xml b/AAmusic/app/src/main/res/drawable-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..7353dbd
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/drawable-xhdpi/ic_launcher.webp b/AAmusic/app/src/main/res/drawable-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..467a9b6
Binary files /dev/null and b/AAmusic/app/src/main/res/drawable-xhdpi/ic_launcher.webp differ
diff --git a/AAmusic/app/src/main/res/drawable-xhdpi/ic_launcher_round.webp b/AAmusic/app/src/main/res/drawable-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..467a9b6
Binary files /dev/null and b/AAmusic/app/src/main/res/drawable-xhdpi/ic_launcher_round.webp differ
diff --git a/AAmusic/app/src/main/res/drawable-xxhdpi/bg_playing_default.webp b/AAmusic/app/src/main/res/drawable-xxhdpi/bg_playing_default.webp
new file mode 100644
index 0000000..64cea45
Binary files /dev/null and b/AAmusic/app/src/main/res/drawable-xxhdpi/bg_playing_default.webp differ
diff --git a/AAmusic/app/src/main/res/drawable-xxhdpi/bg_playing_default_cover.webp b/AAmusic/app/src/main/res/drawable-xxhdpi/bg_playing_default_cover.webp
new file mode 100644
index 0000000..6edd2aa
Binary files /dev/null and b/AAmusic/app/src/main/res/drawable-xxhdpi/bg_playing_default_cover.webp differ
diff --git a/AAmusic/app/src/main/res/drawable-xxhdpi/bg_playing_disc.webp b/AAmusic/app/src/main/res/drawable-xxhdpi/bg_playing_disc.webp
new file mode 100644
index 0000000..ed05f79
Binary files /dev/null and b/AAmusic/app/src/main/res/drawable-xxhdpi/bg_playing_disc.webp differ
diff --git a/AAmusic/app/src/main/res/drawable-xxhdpi/ic_default_artist.webp b/AAmusic/app/src/main/res/drawable-xxhdpi/ic_default_artist.webp
new file mode 100644
index 0000000..ccbb164
Binary files /dev/null and b/AAmusic/app/src/main/res/drawable-xxhdpi/ic_default_artist.webp differ
diff --git a/AAmusic/app/src/main/res/drawable-xxhdpi/ic_default_cover.webp b/AAmusic/app/src/main/res/drawable-xxhdpi/ic_default_cover.webp
new file mode 100644
index 0000000..0b62134
Binary files /dev/null and b/AAmusic/app/src/main/res/drawable-xxhdpi/ic_default_cover.webp differ
diff --git a/AAmusic/app/src/main/res/drawable-xxhdpi/ic_launcher.webp b/AAmusic/app/src/main/res/drawable-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..356a825
Binary files /dev/null and b/AAmusic/app/src/main/res/drawable-xxhdpi/ic_launcher.webp differ
diff --git a/AAmusic/app/src/main/res/drawable-xxhdpi/ic_launcher_round.webp b/AAmusic/app/src/main/res/drawable-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..356a825
Binary files /dev/null and b/AAmusic/app/src/main/res/drawable-xxhdpi/ic_launcher_round.webp differ
diff --git a/AAmusic/app/src/main/res/drawable-xxhdpi/ic_playing_needle.webp b/AAmusic/app/src/main/res/drawable-xxhdpi/ic_playing_needle.webp
new file mode 100644
index 0000000..f9f9c11
Binary files /dev/null and b/AAmusic/app/src/main/res/drawable-xxhdpi/ic_playing_needle.webp differ
diff --git a/AAmusic/app/src/main/res/drawable-xxhdpi/ic_sound_wave_1.webp b/AAmusic/app/src/main/res/drawable-xxhdpi/ic_sound_wave_1.webp
new file mode 100644
index 0000000..2992a69
Binary files /dev/null and b/AAmusic/app/src/main/res/drawable-xxhdpi/ic_sound_wave_1.webp differ
diff --git a/AAmusic/app/src/main/res/drawable-xxhdpi/ic_sound_wave_2.webp b/AAmusic/app/src/main/res/drawable-xxhdpi/ic_sound_wave_2.webp
new file mode 100644
index 0000000..69989c3
Binary files /dev/null and b/AAmusic/app/src/main/res/drawable-xxhdpi/ic_sound_wave_2.webp differ
diff --git a/AAmusic/app/src/main/res/drawable-xxhdpi/ic_sound_wave_3.webp b/AAmusic/app/src/main/res/drawable-xxhdpi/ic_sound_wave_3.webp
new file mode 100644
index 0000000..dfd6c5f
Binary files /dev/null and b/AAmusic/app/src/main/res/drawable-xxhdpi/ic_sound_wave_3.webp differ
diff --git a/AAmusic/app/src/main/res/drawable-xxhdpi/ic_sound_wave_4.webp b/AAmusic/app/src/main/res/drawable-xxhdpi/ic_sound_wave_4.webp
new file mode 100644
index 0000000..316002a
Binary files /dev/null and b/AAmusic/app/src/main/res/drawable-xxhdpi/ic_sound_wave_4.webp differ
diff --git a/AAmusic/app/src/main/res/drawable-xxxhdpi/ic_launcher.webp b/AAmusic/app/src/main/res/drawable-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..f2f8990
Binary files /dev/null and b/AAmusic/app/src/main/res/drawable-xxxhdpi/ic_launcher.webp differ
diff --git a/AAmusic/app/src/main/res/drawable-xxxhdpi/ic_launcher_round.webp b/AAmusic/app/src/main/res/drawable-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..f2f8990
Binary files /dev/null and b/AAmusic/app/src/main/res/drawable-xxxhdpi/ic_launcher_round.webp differ
diff --git a/AAmusic/app/src/main/res/drawable/bg_card.xml b/AAmusic/app/src/main/res/drawable/bg_card.xml
new file mode 100644
index 0000000..38372c9
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/bg_card.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/drawable/bg_play_bar_progress.xml b/AAmusic/app/src/main/res/drawable/bg_play_bar_progress.xml
new file mode 100644
index 0000000..d3a23f0
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/bg_play_bar_progress.xml
@@ -0,0 +1,15 @@
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/drawable/bg_playing_cover_border.xml b/AAmusic/app/src/main/res/drawable/bg_playing_cover_border.xml
new file mode 100644
index 0000000..3207023
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/bg_playing_cover_border.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/drawable/bg_playing_playback_progress.xml b/AAmusic/app/src/main/res/drawable/bg_playing_playback_progress.xml
new file mode 100644
index 0000000..1c9e79e
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/bg_playing_playback_progress.xml
@@ -0,0 +1,25 @@
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/drawable/bg_playing_volume_progress.xml b/AAmusic/app/src/main/res/drawable/bg_playing_volume_progress.xml
new file mode 100644
index 0000000..a3c0f4a
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/bg_playing_volume_progress.xml
@@ -0,0 +1,17 @@
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/drawable/ic_arrow_down.xml b/AAmusic/app/src/main/res/drawable/ic_arrow_down.xml
new file mode 100644
index 0000000..ba2142a
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_arrow_down.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_arrow_right.xml b/AAmusic/app/src/main/res/drawable/ic_arrow_right.xml
new file mode 100644
index 0000000..4932b25
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_arrow_right.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_close.xml b/AAmusic/app/src/main/res/drawable/ic_close.xml
new file mode 100644
index 0000000..da93aff
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_close.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_delete.xml b/AAmusic/app/src/main/res/drawable/ic_delete.xml
new file mode 100644
index 0000000..a54b7b3
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_delete.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_discovery_playlist.xml b/AAmusic/app/src/main/res/drawable/ic_discovery_playlist.xml
new file mode 100644
index 0000000..fa1fba0
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_discovery_playlist.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_discovery_rank.xml b/AAmusic/app/src/main/res/drawable/ic_discovery_rank.xml
new file mode 100644
index 0000000..dd3eef5
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_discovery_rank.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_download.xml b/AAmusic/app/src/main/res/drawable/ic_download.xml
new file mode 100644
index 0000000..685a6ce
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_download.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_favorite.xml b/AAmusic/app/src/main/res/drawable/ic_favorite.xml
new file mode 100644
index 0000000..5071dc1
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_favorite.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_favorite_fill.xml b/AAmusic/app/src/main/res/drawable/ic_favorite_fill.xml
new file mode 100644
index 0000000..2bcc3c9
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_favorite_fill.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_favorite_selector.xml b/AAmusic/app/src/main/res/drawable/ic_favorite_selector.xml
new file mode 100644
index 0000000..fd70774
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_favorite_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/drawable/ic_launcher_foreground.xml b/AAmusic/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..07c13e2
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_local_music.xml b/AAmusic/app/src/main/res/drawable/ic_local_music.xml
new file mode 100644
index 0000000..35e28ce
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_local_music.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_menu.xml b/AAmusic/app/src/main/res/drawable/ic_menu.xml
new file mode 100644
index 0000000..7915d80
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_menu.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_menu_about.xml b/AAmusic/app/src/main/res/drawable/ic_menu_about.xml
new file mode 100644
index 0000000..1ece034
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_menu_about.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_menu_domain.xml b/AAmusic/app/src/main/res/drawable/ic_menu_domain.xml
new file mode 100644
index 0000000..af0776a
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_menu_domain.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_menu_exit.xml b/AAmusic/app/src/main/res/drawable/ic_menu_exit.xml
new file mode 100644
index 0000000..5da22ad
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_menu_exit.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_menu_logout.xml b/AAmusic/app/src/main/res/drawable/ic_menu_logout.xml
new file mode 100644
index 0000000..c4577b2
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_menu_logout.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_menu_search.xml b/AAmusic/app/src/main/res/drawable/ic_menu_search.xml
new file mode 100644
index 0000000..20c7b4e
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_menu_search.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_menu_settings.xml b/AAmusic/app/src/main/res/drawable/ic_menu_settings.xml
new file mode 100644
index 0000000..95fc144
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_menu_settings.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_menu_timer.xml b/AAmusic/app/src/main/res/drawable/ic_menu_timer.xml
new file mode 100644
index 0000000..b3b5c74
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_menu_timer.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_more.xml b/AAmusic/app/src/main/res/drawable/ic_more.xml
new file mode 100644
index 0000000..4714416
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_more.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_next.xml b/AAmusic/app/src/main/res/drawable/ic_next.xml
new file mode 100644
index 0000000..9d0693f
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_next.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_notification.xml b/AAmusic/app/src/main/res/drawable/ic_notification.xml
new file mode 100644
index 0000000..7a0582f
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_notification.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_notification_next.xml b/AAmusic/app/src/main/res/drawable/ic_notification_next.xml
new file mode 100644
index 0000000..a32e2f2
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_notification_next.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_notification_pause.xml b/AAmusic/app/src/main/res/drawable/ic_notification_pause.xml
new file mode 100644
index 0000000..74a29c8
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_notification_pause.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_notification_play.xml b/AAmusic/app/src/main/res/drawable/ic_notification_play.xml
new file mode 100644
index 0000000..a11c963
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_notification_play.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_pause.xml b/AAmusic/app/src/main/res/drawable/ic_pause.xml
new file mode 100644
index 0000000..91049a3
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_pause.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_pause_circle.xml b/AAmusic/app/src/main/res/drawable/ic_pause_circle.xml
new file mode 100644
index 0000000..fabccea
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_pause_circle.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_play.xml b/AAmusic/app/src/main/res/drawable/ic_play.xml
new file mode 100644
index 0000000..4c6a158
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_play.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_play_bar_play_pause_selector.xml b/AAmusic/app/src/main/res/drawable/ic_play_bar_play_pause_selector.xml
new file mode 100644
index 0000000..90b80a7
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_play_bar_play_pause_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/drawable/ic_play_circle.xml b/AAmusic/app/src/main/res/drawable/ic_play_circle.xml
new file mode 100644
index 0000000..d257359
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_play_circle.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_play_mode_level_list.xml b/AAmusic/app/src/main/res/drawable/ic_play_mode_level_list.xml
new file mode 100644
index 0000000..f650d04
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_play_mode_level_list.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/drawable/ic_play_mode_loop.xml b/AAmusic/app/src/main/res/drawable/ic_play_mode_loop.xml
new file mode 100644
index 0000000..caf3924
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_play_mode_loop.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_play_mode_shuffle.xml b/AAmusic/app/src/main/res/drawable/ic_play_mode_shuffle.xml
new file mode 100644
index 0000000..b2e3d92
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_play_mode_shuffle.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_play_mode_single.xml b/AAmusic/app/src/main/res/drawable/ic_play_mode_single.xml
new file mode 100644
index 0000000..7b0c7c0
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_play_mode_single.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_playing_play_pause_selector.xml b/AAmusic/app/src/main/res/drawable/ic_playing_play_pause_selector.xml
new file mode 100644
index 0000000..274c8b1
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_playing_play_pause_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/drawable/ic_playing_playback_progress_thumb.xml b/AAmusic/app/src/main/res/drawable/ic_playing_playback_progress_thumb.xml
new file mode 100644
index 0000000..986e73e
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_playing_playback_progress_thumb.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/drawable/ic_playing_volume_progress_thumb.xml b/AAmusic/app/src/main/res/drawable/ic_playing_volume_progress_thumb.xml
new file mode 100644
index 0000000..385a58c
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_playing_volume_progress_thumb.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/drawable/ic_playlist.xml b/AAmusic/app/src/main/res/drawable/ic_playlist.xml
new file mode 100644
index 0000000..e080e61
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_playlist.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_previous.xml b/AAmusic/app/src/main/res/drawable/ic_previous.xml
new file mode 100644
index 0000000..6e00afb
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_previous.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_radio.xml b/AAmusic/app/src/main/res/drawable/ic_radio.xml
new file mode 100644
index 0000000..e7e1551
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_radio.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_recommend.xml b/AAmusic/app/src/main/res/drawable/ic_recommend.xml
new file mode 100644
index 0000000..d2e06b0
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_recommend.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_sound_wave_animation.xml b/AAmusic/app/src/main/res/drawable/ic_sound_wave_animation.xml
new file mode 100644
index 0000000..33dd3c6
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_sound_wave_animation.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/drawable/ic_tab_discover.xml b/AAmusic/app/src/main/res/drawable/ic_tab_discover.xml
new file mode 100644
index 0000000..27fefc3
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_tab_discover.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_tab_layout_indicator.xml b/AAmusic/app/src/main/res/drawable/ic_tab_layout_indicator.xml
new file mode 100644
index 0000000..d9fc788
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_tab_layout_indicator.xml
@@ -0,0 +1,14 @@
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/drawable/ic_tab_mine.xml b/AAmusic/app/src/main/res/drawable/ic_tab_mine.xml
new file mode 100644
index 0000000..c0932df
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_tab_mine.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/drawable/ic_volume.xml b/AAmusic/app/src/main/res/drawable/ic_volume.xml
new file mode 100644
index 0000000..a6f8404
--- /dev/null
+++ b/AAmusic/app/src/main/res/drawable/ic_volume.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/AAmusic/app/src/main/res/layout/activity_about.xml b/AAmusic/app/src/main/res/layout/activity_about.xml
new file mode 100644
index 0000000..aef3ea8
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/activity_about.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/AAmusic/app/src/main/res/layout/activity_main.xml b/AAmusic/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..1ef799b
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AAmusic/app/src/main/res/layout/activity_playing.xml b/AAmusic/app/src/main/res/layout/activity_playing.xml
new file mode 100644
index 0000000..407c63f
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/activity_playing.xml
@@ -0,0 +1,328 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/activity_settings.xml b/AAmusic/app/src/main/res/layout/activity_settings.xml
new file mode 100644
index 0000000..aef3ea8
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/activity_settings.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/AAmusic/app/src/main/res/layout/dialog_api_domain.xml b/AAmusic/app/src/main/res/layout/dialog_api_domain.xml
new file mode 100644
index 0000000..530ec1b
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/dialog_api_domain.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/dialog_song_more_menu.xml b/AAmusic/app/src/main/res/layout/dialog_song_more_menu.xml
new file mode 100644
index 0000000..d03e8cf
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/dialog_song_more_menu.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/fragment_collect_song.xml b/AAmusic/app/src/main/res/layout/fragment_collect_song.xml
new file mode 100644
index 0000000..e465653
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/fragment_collect_song.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
diff --git a/AAmusic/app/src/main/res/layout/fragment_current_playlist.xml b/AAmusic/app/src/main/res/layout/fragment_current_playlist.xml
new file mode 100644
index 0000000..55783ef
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/fragment_current_playlist.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AAmusic/app/src/main/res/layout/fragment_discover.xml b/AAmusic/app/src/main/res/layout/fragment_discover.xml
new file mode 100644
index 0000000..b3aa5dc
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/fragment_discover.xml
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/fragment_local_music.xml b/AAmusic/app/src/main/res/layout/fragment_local_music.xml
new file mode 100644
index 0000000..aba3acf
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/fragment_local_music.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/fragment_login_route.xml b/AAmusic/app/src/main/res/layout/fragment_login_route.xml
new file mode 100644
index 0000000..0f36c22
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/fragment_login_route.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/fragment_mine.xml b/AAmusic/app/src/main/res/layout/fragment_mine.xml
new file mode 100644
index 0000000..9979c2f
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/fragment_mine.xml
@@ -0,0 +1,172 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/fragment_phone_login.xml b/AAmusic/app/src/main/res/layout/fragment_phone_login.xml
new file mode 100644
index 0000000..457fdcf
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/fragment_phone_login.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/fragment_playlist_detail.xml b/AAmusic/app/src/main/res/layout/fragment_playlist_detail.xml
new file mode 100644
index 0000000..952b431
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/fragment_playlist_detail.xml
@@ -0,0 +1,219 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/fragment_playlist_spuare.xml b/AAmusic/app/src/main/res/layout/fragment_playlist_spuare.xml
new file mode 100644
index 0000000..a023337
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/fragment_playlist_spuare.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/fragment_qrcode_login.xml b/AAmusic/app/src/main/res/layout/fragment_qrcode_login.xml
new file mode 100644
index 0000000..f686705
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/fragment_qrcode_login.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/fragment_ranking.xml b/AAmusic/app/src/main/res/layout/fragment_ranking.xml
new file mode 100644
index 0000000..0b5323a
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/fragment_ranking.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/fragment_recommend_song.xml b/AAmusic/app/src/main/res/layout/fragment_recommend_song.xml
new file mode 100644
index 0000000..38a2d74
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/fragment_recommend_song.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/fragment_search.xml b/AAmusic/app/src/main/res/layout/fragment_search.xml
new file mode 100644
index 0000000..3ba460b
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/fragment_search.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/item_current_playlist.xml b/AAmusic/app/src/main/res/layout/item_current_playlist.xml
new file mode 100644
index 0000000..87a33a1
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/item_current_playlist.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/item_discover_playlist.xml b/AAmusic/app/src/main/res/layout/item_discover_playlist.xml
new file mode 100644
index 0000000..56847e5
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/item_discover_playlist.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AAmusic/app/src/main/res/layout/item_discover_ranking.xml b/AAmusic/app/src/main/res/layout/item_discover_ranking.xml
new file mode 100644
index 0000000..98cd1d8
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/item_discover_ranking.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/item_discover_ranking_song.xml b/AAmusic/app/src/main/res/layout/item_discover_ranking_song.xml
new file mode 100644
index 0000000..02ad1c0
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/item_discover_ranking_song.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/item_local_song.xml b/AAmusic/app/src/main/res/layout/item_local_song.xml
new file mode 100644
index 0000000..c13b87b
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/item_local_song.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/item_official_ranking.xml b/AAmusic/app/src/main/res/layout/item_official_ranking.xml
new file mode 100644
index 0000000..500c1eb
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/item_official_ranking.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/item_official_ranking_song.xml b/AAmusic/app/src/main/res/layout/item_official_ranking_song.xml
new file mode 100644
index 0000000..7d8b275
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/item_official_ranking_song.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/item_playlist_song.xml b/AAmusic/app/src/main/res/layout/item_playlist_song.xml
new file mode 100644
index 0000000..09e3178
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/item_playlist_song.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/item_playlist_tag.xml b/AAmusic/app/src/main/res/layout/item_playlist_tag.xml
new file mode 100644
index 0000000..1fe5142
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/item_playlist_tag.xml
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/item_ranking_title.xml b/AAmusic/app/src/main/res/layout/item_ranking_title.xml
new file mode 100644
index 0000000..cb725db
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/item_ranking_title.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/item_recommend_song.xml b/AAmusic/app/src/main/res/layout/item_recommend_song.xml
new file mode 100644
index 0000000..b83c4a4
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/item_recommend_song.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/item_search_history.xml b/AAmusic/app/src/main/res/layout/item_search_history.xml
new file mode 100644
index 0000000..c6f1d6a
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/item_search_history.xml
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/item_search_playlist.xml b/AAmusic/app/src/main/res/layout/item_search_playlist.xml
new file mode 100644
index 0000000..73d8e31
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/item_search_playlist.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/item_search_song.xml b/AAmusic/app/src/main/res/layout/item_search_song.xml
new file mode 100644
index 0000000..d9e0114
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/item_search_song.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/item_selected_ranking.xml b/AAmusic/app/src/main/res/layout/item_selected_ranking.xml
new file mode 100644
index 0000000..47643a5
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/item_selected_ranking.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/item_song_more_menu.xml b/AAmusic/app/src/main/res/layout/item_song_more_menu.xml
new file mode 100644
index 0000000..632145e
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/item_song_more_menu.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/item_user_playlist.xml b/AAmusic/app/src/main/res/layout/item_user_playlist.xml
new file mode 100644
index 0000000..eb8e929
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/item_user_playlist.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/layout_play_bar.xml b/AAmusic/app/src/main/res/layout/layout_play_bar.xml
new file mode 100644
index 0000000..166ba4e
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/layout_play_bar.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/load_sir_loading_sound_wave.xml b/AAmusic/app/src/main/res/layout/load_sir_loading_sound_wave.xml
new file mode 100644
index 0000000..6e9ef1e
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/load_sir_loading_sound_wave.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/navigation_header.xml b/AAmusic/app/src/main/res/layout/navigation_header.xml
new file mode 100644
index 0000000..be01393
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/navigation_header.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/notification.xml b/AAmusic/app/src/main/res/layout/notification.xml
new file mode 100644
index 0000000..f50e923
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/notification.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/tab_item.xml b/AAmusic/app/src/main/res/layout/tab_item.xml
new file mode 100644
index 0000000..f2b421f
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/tab_item.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/title_discover.xml b/AAmusic/app/src/main/res/layout/title_discover.xml
new file mode 100644
index 0000000..89cd1ac
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/title_discover.xml
@@ -0,0 +1,18 @@
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/layout/title_search.xml b/AAmusic/app/src/main/res/layout/title_search.xml
new file mode 100644
index 0000000..c197d33
--- /dev/null
+++ b/AAmusic/app/src/main/res/layout/title_search.xml
@@ -0,0 +1,23 @@
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/menu/navigation_menu.xml b/AAmusic/app/src/main/res/menu/navigation_menu.xml
new file mode 100644
index 0000000..7e22471
--- /dev/null
+++ b/AAmusic/app/src/main/res/menu/navigation_menu.xml
@@ -0,0 +1,25 @@
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/values-night/colors.xml b/AAmusic/app/src/main/res/values-night/colors.xml
new file mode 100644
index 0000000..9e12bbe
--- /dev/null
+++ b/AAmusic/app/src/main/res/values-night/colors.xml
@@ -0,0 +1,13 @@
+
+
+ @color/red_700
+ #303030
+ @color/black
+ @color/white
+ #9E9E9E
+ @color/grey_800
+
+ @color/black
+ @color/grey_800
+ @color/grey_900
+
diff --git a/AAmusic/app/src/main/res/values/arrays.xml b/AAmusic/app/src/main/res/values/arrays.xml
new file mode 100644
index 0000000..2055139
--- /dev/null
+++ b/AAmusic/app/src/main/res/values/arrays.xml
@@ -0,0 +1,93 @@
+
+
+
+
+ - 不开启
+ - 10分钟后
+ - 20分钟后
+ - 30分钟后
+ - 45分钟后
+ - 60分钟后
+ - 90分钟后
+
+
+
+ - 0
+ - 10
+ - 20
+ - 30
+ - 45
+ - 60
+ - 90
+
+
+
+ - 跟随系统
+ - 浅色
+ - 深色
+
+
+
+ - 0
+ - 1
+ - 2
+
+
+
+ - 标准
+ - 较高
+ - 极高(HQ)
+ - 无损(SQ)
+ - 高解析度无损(Hi-Res)🇻
+ - 高清环绕声(Spatial Audio)🇻
+ - 沉浸环绕声(Surround Audio)🇸
+ - 超清母带(Master)🇸
+
+
+
+ - standard
+ - higher
+ - exhigh
+ - lossless
+ - hires
+ - jyeffect
+ - sky
+ - jymaster
+
+
+
+ - 不过滤
+ - 100KB
+ - 200KB
+ - 500KB
+ - 1MB
+ - 2MB
+
+
+
+ - 0
+ - 100
+ - 200
+ - 500
+ - 1024
+ - 2048
+
+
+
+ - 不过滤
+ - 15秒
+ - 30秒
+ - 1分钟
+ - 1分30秒
+ - 2分钟
+
+
+
+ - 0
+ - 15
+ - 30
+ - 60
+ - 90
+ - 120
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/values/attrs.xml b/AAmusic/app/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..bf8340f
--- /dev/null
+++ b/AAmusic/app/src/main/res/values/attrs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/values/colors.xml b/AAmusic/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..988d482
--- /dev/null
+++ b/AAmusic/app/src/main/res/values/colors.xml
@@ -0,0 +1,30 @@
+
+
+ #F44336
+ #D32F2F
+ #4CF44336
+ #F5FFFFFF
+ #2196F3
+ #9E9E9E
+ #809E9E9E
+ #E5E5E5
+ #E0E0E0
+ #F5616161
+ #F5E0E0E0
+ #424242
+ #212121
+ #F5212121
+ @color/red_500
+
+ @color/red_500
+ #FAFAFA
+ @color/common_theme_color
+ @color/white
+ #202020
+ @color/grey
+ @color/grey_100
+
+ @color/white
+ @color/white
+ @color/white
+
diff --git a/AAmusic/app/src/main/res/values/dimens.xml b/AAmusic/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..f4bdb62
--- /dev/null
+++ b/AAmusic/app/src/main/res/values/dimens.xml
@@ -0,0 +1,3 @@
+
+ 48dp
+
diff --git a/AAmusic/app/src/main/res/values/ids.xml b/AAmusic/app/src/main/res/values/ids.xml
new file mode 100644
index 0000000..02bc7b9
--- /dev/null
+++ b/AAmusic/app/src/main/res/values/ids.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/values/strings.xml b/AAmusic/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..3397a4b
--- /dev/null
+++ b/AAmusic/app/src/main/res/values/strings.xml
@@ -0,0 +1,50 @@
+
+ AA音乐
+ 分享
+ 删除
+ 搜索
+ 保存
+ 努力加载中…
+ 加载失败,请检查网络后重试
+ 本地音乐
+ 在线音乐
+ 未知
+ 删除%s?
+ 设置铃声成功
+ 暂无本地音乐
+ 列表循环
+ 随机播放
+ 单曲循环
+ 00:00
+ %s下载完成
+ 暂时无法播放
+ 暂时无法下载
+ 暂时无法分享
+ 暂无歌手信息
+ 歌曲名、歌手名
+ 未找到相关结果
+ 功能设置
+ 推荐给朋友
+ 关于
+ 夜间模式
+ 定时停止播放
+ 关闭应用
+ 播放
+ 音效调节
+ 温馨提示
+ 当前处于移动网络,确定打开移动网络播放开关吗?
+ 立即打开
+ 当前处于移动网络,下载将消耗较多流量,是否继续下载?
+ 继续下载
+ 您的设备不支持此功能
+ 设置成功,将于%s分钟后关闭
+ 定时停止播放已取消
+ 青春,狂热,放纵,自由,当我们带上耳机以后做回自己,这是音乐的魅力。%1$s:%2$s
+ https://github.com/wangchenyan/ponymusic
+
+ 歌曲信息
+ 没有存储空间权限,无法扫描本地歌曲!
+ 没有权限,无法设置铃声,请授予权限
+ 授权成功,请在再次操作以设置铃声
+ 没有权限,无法选择图片!
+
diff --git a/AAmusic/app/src/main/res/values/strings_preference.xml b/AAmusic/app/src/main/res/values/strings_preference.xml
new file mode 100644
index 0000000..96749e9
--- /dev/null
+++ b/AAmusic/app/src/main/res/values/strings_preference.xml
@@ -0,0 +1,9 @@
+
+
+ dark_mode
+ play_sound_quality
+ download_sound_quality
+ sound_effect
+ filter_size
+ filter_time
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/values/styles.xml b/AAmusic/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..5a1f8b8
--- /dev/null
+++ b/AAmusic/app/src/main/res/values/styles.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AAmusic/app/src/main/res/xml/network_security_config.xml b/AAmusic/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 0000000..0a511fc
--- /dev/null
+++ b/AAmusic/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AAmusic/app/src/main/res/xml/preference_about.xml b/AAmusic/app/src/main/res/xml/preference_about.xml
new file mode 100644
index 0000000..05927b8
--- /dev/null
+++ b/AAmusic/app/src/main/res/xml/preference_about.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/app/src/main/res/xml/preference_setting.xml b/AAmusic/app/src/main/res/xml/preference_setting.xml
new file mode 100644
index 0000000..15133b2
--- /dev/null
+++ b/AAmusic/app/src/main/res/xml/preference_setting.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AAmusic/build.gradle.kts b/AAmusic/build.gradle.kts
new file mode 100644
index 0000000..e3d4ac0
--- /dev/null
+++ b/AAmusic/build.gradle.kts
@@ -0,0 +1,20 @@
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.kotlin) apply false
+ alias(libs.plugins.ksp) apply false
+ if (File("app/google-services.json").exists()) {
+ println("enable gms in root plugins")
+ alias(libs.plugins.gms) apply false
+ alias(libs.plugins.crashlytics) apply false
+ }
+ alias(libs.plugins.hilt) apply false
+}
+
+buildscript {
+ dependencies {
+ classpath(libs.autoRegister)
+ // fix r8 build error
+ classpath(libs.r8)
+ }
+}
diff --git a/AAmusic/gradle.properties b/AAmusic/gradle.properties
new file mode 100644
index 0000000..f330bd9
--- /dev/null
+++ b/AAmusic/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+android.useAndroidX=true
+android.enableJetifier=true
+android.nonTransitiveRClass=false
diff --git a/AAmusic/gradle/libs.versions.toml b/AAmusic/gradle/libs.versions.toml
new file mode 100644
index 0000000..913dd1f
--- /dev/null
+++ b/AAmusic/gradle/libs.versions.toml
@@ -0,0 +1,91 @@
+[versions]
+# android sdk
+compileSdk = "34"
+targetSdk = "34"
+minSdk = "21"
+versionCode = "2030001"
+versionName = "2.3.0-beta01"
+# java
+java = "VERSION_1_8"
+jvmTarget = "1.8"
+# plugin
+agp = "7.4.2"
+kotlin = "1.9.10"
+ksp = "1.9.10-1.0.13"
+hilt = "2.48.1"
+# sdk
+media3 = "1.3.0"
+room = "2.6.0"
+common = "1.0.0-beta12"
+crouter = "2.4.0-beta01"
+firebaseCrashlyticsBuildtools = "3.0.0"
+supportAnnotations = "28.0.0"
+annotation = "1.6.0"
+supportV4 = "28.0.0"
+legacySupportV4 = "1.0.0"
+supportV13 = "28.0.0"
+legacySupportV13 = "1.0.0"
+appcompatV7 = "28.0.0"
+supportVectorDrawable = "28.0.0"
+vectordrawable = "1.1.0"
+design = "28.0.0"
+gridlayoutV7 = "28.0.0"
+gridlayout = "1.0.0"
+kotlinReflect = "1.9.20"
+error_prone_annotations = "2.27.1"
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+android-library = { id = "com.android.library", version.ref = "agp" }
+kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+gms = { id = "com.google.gms.google-services", version = "4.4.0" }
+crashlytics = { id = "com.google.firebase.crashlytics", version = "2.9.5" }
+ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
+hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
+
+[libraries]
+# plugin
+autoRegister = { group = "com.github.wangchenyan", name = "AutoRegister", version = "1.4.3" }
+r8 = { group = "com.android.tools", name = "r8", version = "8.3.37" }
+# androidx
+appcompat = { group = "androidx.appcompat", name = "appcompat", version = "1.6.1" }
+material = { group = "com.google.android.material", name = "material", version = "1.10.0" }
+constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version = "2.1.4" }
+flexbox = { group = "com.google.android.flexbox", name = "flexbox", version = "3.0.0" }
+media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" }
+media3-datasource-okhttp = { group = "androidx.media3", name = "media3-datasource-okhttp", version.ref = "media3" }
+media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" }
+media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" }
+room = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
+room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
+preference = { group = "androidx.preference", name = "preference-ktx", version = "1.2.1" }
+# google
+crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx", version = "18.5.0" }
+analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx", version = "21.4.0" }
+hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
+hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
+# wangchenyan
+common = { group = "com.github.wangchenyan", name = "android-common", version.ref = "common" }
+crouter-compiler = { group = "com.github.wangchenyan.crouter", name = "crouter-compiler", version.ref = "crouter" }
+crouter-api = { group = "com.github.wangchenyan.crouter", name = "crouter-api", version.ref = "crouter" }
+lrcview = { group = "com.github.wangchenyan", name = "lrcview", version = "2.2.1" }
+# open source
+loggingInterceptor = { group = "com.github.ihsanbal", name = "LoggingInterceptor", version = "3.1.0" }
+zbar = { group = "cn.bertsir.zbarLibary", name = "zbarlibary", version = "1.4.2" }
+blurry = { group = "jp.wasabeef", name = "blurry", version = "4.0.1" }
+banner = { group = "io.github.youth5201314", name = "banner", version = "2.2.2" }
+firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
+support-annotations = { group = "com.android.support", name = "support-annotations", version.ref = "supportAnnotations" }
+annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" }
+support-v4 = { group = "com.android.support", name = "support-v4", version.ref = "supportV4" }
+legacy-support-v4 = { group = "androidx.legacy", name = "legacy-support-v4", version.ref = "legacySupportV4" }
+support-v13 = { group = "com.android.support", name = "support-v13", version.ref = "supportV13" }
+legacy-support-v13 = { group = "androidx.legacy", name = "legacy-support-v13", version.ref = "legacySupportV13" }
+appcompat-v7 = { group = "com.android.support", name = "appcompat-v7", version.ref = "appcompatV7" }
+support-vector-drawable = { group = "com.android.support", name = "support-vector-drawable", version.ref = "supportVectorDrawable" }
+vectordrawable = { group = "androidx.vectordrawable", name = "vectordrawable", version.ref = "vectordrawable" }
+design = { group = "com.android.support", name = "design", version.ref = "design" }
+gridlayout-v7 = { group = "com.android.support", name = "gridlayout-v7", version.ref = "gridlayoutV7" }
+gridlayout = { group = "androidx.gridlayout", name = "gridlayout", version.ref = "gridlayout" }
+kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlinReflect" }
+error_prone_annotations = { group = "com.google.errorprone", name = "error_prone_annotations", version.ref = "error_prone_annotations" }
diff --git a/AAmusic/gradle/wrapper/gradle-wrapper.jar b/AAmusic/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..05ef575
Binary files /dev/null and b/AAmusic/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/AAmusic/gradle/wrapper/gradle-wrapper.properties b/AAmusic/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..ac5cc79
--- /dev/null
+++ b/AAmusic/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Jan 13 13:27:59 CST 2021
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
diff --git a/AAmusic/gradlew b/AAmusic/gradlew
new file mode 100644
index 0000000..9d82f78
--- /dev/null
+++ b/AAmusic/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/AAmusic/gradlew.bat b/AAmusic/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/AAmusic/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/AAmusic/settings.gradle.kts b/AAmusic/settings.gradle.kts
new file mode 100644
index 0000000..577a7b2
--- /dev/null
+++ b/AAmusic/settings.gradle.kts
@@ -0,0 +1,22 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ maven("https://maven.aliyun.com/repository/public/")
+ maven("https://jitpack.io")
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ maven("https://maven.aliyun.com/repository/public/")
+ maven("https://jitpack.io")
+ maven("https://repo1.maven.org/maven2/")
+ // banner
+ maven("https://s01.oss.sonatype.org/content/groups/public")
+ }
+}
+include(":app")