merge branch 'master' into buffering-indicator
This commit is contained in:
commit
185076c4ec
12 changed files with 140 additions and 77 deletions
|
|
@ -8,8 +8,8 @@ android {
|
||||||
minSdkVersion 19
|
minSdkVersion 19
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
|
|
||||||
versionCode 8
|
versionCode 9
|
||||||
versionName '1.2.0'
|
versionName '1.2.1'
|
||||||
|
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,8 @@ public class App extends Application {
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
// enable for all builds to help with bug reports
|
||||||
RedScreenOfDeath.init(this);
|
RedScreenOfDeath.init(this);
|
||||||
}
|
|
||||||
|
|
||||||
app = this;
|
app = this;
|
||||||
database = createDatabase(this);
|
database = createDatabase(this);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.fragment.app.FragmentPagerAdapter;
|
import androidx.fragment.app.FragmentPagerAdapter;
|
||||||
|
|
||||||
|
import com.dkanada.gramophone.fragments.mainactivity.library.pager.FavoritesFragment;
|
||||||
import com.dkanada.gramophone.model.CategoryInfo;
|
import com.dkanada.gramophone.model.CategoryInfo;
|
||||||
import com.dkanada.gramophone.fragments.mainactivity.library.pager.AlbumsFragment;
|
import com.dkanada.gramophone.fragments.mainactivity.library.pager.AlbumsFragment;
|
||||||
import com.dkanada.gramophone.fragments.mainactivity.library.pager.ArtistsFragment;
|
import com.dkanada.gramophone.fragments.mainactivity.library.pager.ArtistsFragment;
|
||||||
|
|
@ -154,7 +155,8 @@ public class MusicLibraryPagerAdapter extends FragmentPagerAdapter {
|
||||||
ALBUMS(AlbumsFragment.class),
|
ALBUMS(AlbumsFragment.class),
|
||||||
ARTISTS(ArtistsFragment.class),
|
ARTISTS(ArtistsFragment.class),
|
||||||
GENRES(GenresFragment.class),
|
GENRES(GenresFragment.class),
|
||||||
PLAYLISTS(PlaylistsFragment.class);
|
PLAYLISTS(PlaylistsFragment.class),
|
||||||
|
FAVORITES(FavoritesFragment.class);
|
||||||
|
|
||||||
private final Class<? extends Fragment> mFragmentClass;
|
private final Class<? extends Fragment> mFragmentClass;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.dkanada.gramophone.databinding.FragmentLibraryBinding;
|
import com.dkanada.gramophone.databinding.FragmentLibraryBinding;
|
||||||
|
import com.dkanada.gramophone.fragments.mainactivity.library.pager.FavoritesFragment;
|
||||||
import com.google.android.material.appbar.AppBarLayout;
|
import com.google.android.material.appbar.AppBarLayout;
|
||||||
import com.afollestad.materialcab.MaterialCab;
|
import com.afollestad.materialcab.MaterialCab;
|
||||||
import com.kabouzeid.appthemehelper.ThemeStore;
|
import com.kabouzeid.appthemehelper.ThemeStore;
|
||||||
|
|
@ -354,7 +355,7 @@ public class LibraryFragment extends AbsMainActivityFragment implements CabHolde
|
||||||
.setChecked(currentSortMethod.equals(SortMethod.ADDED));
|
.setChecked(currentSortMethod.equals(SortMethod.ADDED));
|
||||||
sortMethodMenu.add(0, R.id.action_sort_method_random, 4, R.string.sort_method_random)
|
sortMethodMenu.add(0, R.id.action_sort_method_random, 4, R.string.sort_method_random)
|
||||||
.setChecked(currentSortMethod.equals(SortMethod.RANDOM));
|
.setChecked(currentSortMethod.equals(SortMethod.RANDOM));
|
||||||
} else if (fragment instanceof SongsFragment) {
|
} else if (fragment instanceof SongsFragment || fragment instanceof FavoritesFragment) {
|
||||||
sortMethodMenu.add(0, R.id.action_sort_method_name, 0, R.string.sort_method_name)
|
sortMethodMenu.add(0, R.id.action_sort_method_name, 0, R.string.sort_method_name)
|
||||||
.setChecked(currentSortMethod.equals(SortMethod.NAME));
|
.setChecked(currentSortMethod.equals(SortMethod.NAME));
|
||||||
sortMethodMenu.add(0, R.id.action_sort_method_album, 1, R.string.sort_method_album)
|
sortMethodMenu.add(0, R.id.action_sort_method_album, 1, R.string.sort_method_album)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.dkanada.gramophone.fragments.mainactivity.library.pager;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import com.dkanada.gramophone.App;
|
||||||
|
import com.dkanada.gramophone.util.PreferenceUtil;
|
||||||
|
import com.dkanada.gramophone.util.QueryUtil;
|
||||||
|
import org.jellyfin.apiclient.model.querying.ItemFields;
|
||||||
|
import org.jellyfin.apiclient.model.querying.ItemFilter;
|
||||||
|
import org.jellyfin.apiclient.model.querying.ItemQuery;
|
||||||
|
|
||||||
|
public class FavoritesFragment extends SongsFragment {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
protected ItemQuery createQuery() {
|
||||||
|
ItemQuery query = new ItemQuery();
|
||||||
|
|
||||||
|
query.setIncludeItemTypes(new String[]{"Audio"});
|
||||||
|
query.setFields(new ItemFields[]{ItemFields.MediaSources});
|
||||||
|
query.setUserId(App.getApiClient().getCurrentUserId());
|
||||||
|
query.setRecursive(true);
|
||||||
|
query.setLimit(PreferenceUtil.getInstance(App.getInstance()).getPageSize());
|
||||||
|
query.setStartIndex(getAdapter().getItemCount());
|
||||||
|
query.setParentId(QueryUtil.currentLibrary.getId());
|
||||||
|
query.setFilters(new ItemFilter[]{ItemFilter.IsFavorite});
|
||||||
|
|
||||||
|
QueryUtil.applySortMethod(query, PreferenceUtil.getInstance(App.getInstance()).getSongSortMethod());
|
||||||
|
QueryUtil.applySortOrder(query, PreferenceUtil.getInstance(App.getInstance()).getSongSortOrder());
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,7 +44,8 @@ public class CategoryInfo implements Parcelable {
|
||||||
ALBUMS(R.string.albums),
|
ALBUMS(R.string.albums),
|
||||||
ARTISTS(R.string.artists),
|
ARTISTS(R.string.artists),
|
||||||
GENRES(R.string.genres),
|
GENRES(R.string.genres),
|
||||||
PLAYLISTS(R.string.playlists);
|
PLAYLISTS(R.string.playlists),
|
||||||
|
FAVORITES(R.string.favorites);
|
||||||
|
|
||||||
public final int stringRes;
|
public final int stringRes;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,10 @@ import com.dkanada.gramophone.util.PreferenceUtil;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.ExoPlayer;
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
|
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.FileDataSource;
|
import com.google.android.exoplayer2.upstream.FileDataSource;
|
||||||
|
|
@ -28,27 +26,13 @@ import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvicto
|
||||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import okhttp3.Call;
|
|
||||||
import okhttp3.Callback;
|
|
||||||
import okhttp3.Dispatcher;
|
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Response;
|
|
||||||
import okhttp3.internal.annotations.EverythingIsNonNull;
|
|
||||||
|
|
||||||
public class MultiPlayer implements Playback {
|
public class MultiPlayer implements Playback {
|
||||||
public static final String TAG = MultiPlayer.class.getSimpleName();
|
public static final String TAG = MultiPlayer.class.getSimpleName();
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final OkHttpClient httpClient;
|
private final SimpleExoPlayer exoPlayer;
|
||||||
|
|
||||||
private SimpleExoPlayer exoPlayer;
|
|
||||||
private ConcatenatingMediaSource mediaSource;
|
|
||||||
|
|
||||||
private final SimpleCache simpleCache;
|
private final SimpleCache simpleCache;
|
||||||
private final DataSource.Factory dataSource;
|
|
||||||
|
|
||||||
private PlaybackCallbacks callbacks;
|
private PlaybackCallbacks callbacks;
|
||||||
|
|
||||||
|
|
@ -68,7 +52,7 @@ public class MultiPlayer implements Playback {
|
||||||
int windowIndex = exoPlayer.getCurrentWindowIndex();
|
int windowIndex = exoPlayer.getCurrentWindowIndex();
|
||||||
|
|
||||||
if (windowIndex == 1) {
|
if (windowIndex == 1) {
|
||||||
mediaSource.removeMediaSource(0);
|
exoPlayer.removeMediaItem(0);
|
||||||
if (exoPlayer.isPlaying()) {
|
if (exoPlayer.isPlaying()) {
|
||||||
// there are still songs left in the queue
|
// there are still songs left in the queue
|
||||||
callbacks.onTrackWentToNext();
|
callbacks.onTrackWentToNext();
|
||||||
|
|
@ -88,17 +72,11 @@ public class MultiPlayer implements Playback {
|
||||||
public MultiPlayer(Context context) {
|
public MultiPlayer(Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
||||||
Dispatcher dispatcher = new Dispatcher();
|
MediaSourceFactory mediaSourceFactory = new UnknownMediaSourceFactory(buildDataSourceFactory());
|
||||||
dispatcher.setMaxRequests(1);
|
exoPlayer = new SimpleExoPlayer.Builder(context).setMediaSourceFactory(mediaSourceFactory).build();
|
||||||
|
|
||||||
httpClient = new OkHttpClient.Builder().dispatcher(dispatcher).build();
|
|
||||||
|
|
||||||
exoPlayer = new SimpleExoPlayer.Builder(context).build();
|
|
||||||
mediaSource = new ConcatenatingMediaSource();
|
|
||||||
|
|
||||||
exoPlayer.addListener(eventListener);
|
exoPlayer.addListener(eventListener);
|
||||||
exoPlayer.prepare(mediaSource);
|
exoPlayer.prepare();
|
||||||
exoPlayer.setRepeatMode(Player.REPEAT_MODE_OFF);
|
|
||||||
|
|
||||||
long cacheSize = PreferenceUtil.getInstance(context).getMediaCacheSize();
|
long cacheSize = PreferenceUtil.getInstance(context).getMediaCacheSize();
|
||||||
LeastRecentlyUsedCacheEvictor recentlyUsedCache = new LeastRecentlyUsedCacheEvictor(cacheSize);
|
LeastRecentlyUsedCacheEvictor recentlyUsedCache = new LeastRecentlyUsedCacheEvictor(cacheSize);
|
||||||
|
|
@ -106,23 +84,19 @@ public class MultiPlayer implements Playback {
|
||||||
|
|
||||||
File cacheDirectory = new File(context.getCacheDir(), "exoplayer");
|
File cacheDirectory = new File(context.getCacheDir(), "exoplayer");
|
||||||
simpleCache = new SimpleCache(cacheDirectory, recentlyUsedCache, databaseProvider);
|
simpleCache = new SimpleCache(cacheDirectory, recentlyUsedCache, databaseProvider);
|
||||||
dataSource = buildDataSourceFactory();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setDataSource(Song song) {
|
public void setDataSource(Song song) {
|
||||||
mediaSource = new ConcatenatingMediaSource();
|
exoPlayer.clearMediaItems();
|
||||||
|
|
||||||
exoPlayer.addListener(eventListener);
|
|
||||||
exoPlayer.prepare(mediaSource);
|
|
||||||
|
|
||||||
appendDataSource(MusicUtil.getSongFileUri(song));
|
appendDataSource(MusicUtil.getSongFileUri(song));
|
||||||
|
exoPlayer.seekTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void queueDataSource(Song song) {
|
public void queueDataSource(Song song) {
|
||||||
while (mediaSource.getSize() > 1) {
|
while (exoPlayer.getMediaItemCount() > 1) {
|
||||||
mediaSource.removeMediaSource(1);
|
exoPlayer.removeMediaItem(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
appendDataSource(MusicUtil.getSongFileUri(song));
|
appendDataSource(MusicUtil.getSongFileUri(song));
|
||||||
|
|
@ -130,36 +104,9 @@ public class MultiPlayer implements Playback {
|
||||||
|
|
||||||
private void appendDataSource(String path) {
|
private void appendDataSource(String path) {
|
||||||
Uri uri = Uri.parse(path);
|
Uri uri = Uri.parse(path);
|
||||||
|
MediaItem mediaItem = MediaItem.fromUri(uri);
|
||||||
|
|
||||||
httpClient.newCall(new Request.Builder().url(path).head().build()).enqueue(new Callback() {
|
exoPlayer.addMediaItem(mediaItem);
|
||||||
@Override
|
|
||||||
@EverythingIsNonNull
|
|
||||||
public void onFailure(Call call, IOException e) {
|
|
||||||
Toast.makeText(context, context.getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show();
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@EverythingIsNonNull
|
|
||||||
public void onResponse(Call call, Response response) {
|
|
||||||
String type = response.header("Content-Type");
|
|
||||||
if (type == null) return;
|
|
||||||
|
|
||||||
MediaSource source;
|
|
||||||
if (type.equals("application/x-mpegURL")) {
|
|
||||||
source = new HlsMediaSource.Factory(dataSource)
|
|
||||||
.setTag(path)
|
|
||||||
.setAllowChunklessPreparation(true)
|
|
||||||
.createMediaSource(uri);
|
|
||||||
} else {
|
|
||||||
source = new ProgressiveMediaSource.Factory(dataSource)
|
|
||||||
.setTag(path)
|
|
||||||
.createMediaSource(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaSource.addMediaSource(source);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private DataSource.Factory buildDataSourceFactory() {
|
private DataSource.Factory buildDataSourceFactory() {
|
||||||
|
|
|
||||||
|
|
@ -449,8 +449,8 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
|
||||||
if (restoredPositionInTrack > 0) seek(restoredPositionInTrack);
|
if (restoredPositionInTrack > 0) seek(restoredPositionInTrack);
|
||||||
|
|
||||||
notHandledMetaChangedForCurrentTrack = true;
|
notHandledMetaChangedForCurrentTrack = true;
|
||||||
sendChangeInternal(META_CHANGED);
|
handleChangeInternal(META_CHANGED);
|
||||||
sendChangeInternal(QUEUE_CHANGED);
|
handleChangeInternal(QUEUE_CHANGED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1291,7 +1291,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
|
||||||
info.setItemId(mService.get().getCurrentSong().id);
|
info.setItemId(mService.get().getCurrentSong().id);
|
||||||
info.setPositionTicks(progress * 10000);
|
info.setPositionTicks(progress * 10000);
|
||||||
|
|
||||||
task.cancel(true);
|
if (task != null) task.cancel(true);
|
||||||
executorService.shutdownNow();
|
executorService.shutdownNow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
package com.dkanada.gramophone.service
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.MediaItem
|
||||||
|
import com.google.android.exoplayer2.drm.DrmSessionManager
|
||||||
|
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource
|
||||||
|
import com.google.android.exoplayer2.source.MediaSourceFactory
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||||
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||||
|
import com.google.android.exoplayer2.upstream.*
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
class UnknownMediaSourceFactory(dataSourceFactory: DataSource.Factory) : MediaSourceFactory {
|
||||||
|
private val hlsMediaSource : HlsMediaSource.Factory
|
||||||
|
private val progressiveMediaSource : ProgressiveMediaSource.Factory
|
||||||
|
|
||||||
|
private var loadErrorHandlingPolicy: LoadErrorHandlingPolicy
|
||||||
|
|
||||||
|
override fun setDrmSessionManager(drmSessionManager: DrmSessionManager?): MediaSourceFactory {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setDrmHttpDataSourceFactory(drmHttpDataSourceFactory: HttpDataSource.Factory?): MediaSourceFactory {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setDrmUserAgent(drmUserAgent: String?): MediaSourceFactory {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setLoadErrorHandlingPolicy(loadErrorHandlingPolicy: LoadErrorHandlingPolicy?): MediaSourceFactory {
|
||||||
|
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy!!
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSupportedTypes(): IntArray {
|
||||||
|
return intArrayOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
|
||||||
|
val type: String? = runBlocking {
|
||||||
|
httpGet(mediaItem.playbackProperties!!.uri.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceFactory: MediaSourceFactory = if (type == "application/x-mpegURL") {
|
||||||
|
hlsMediaSource
|
||||||
|
} else {
|
||||||
|
progressiveMediaSource
|
||||||
|
}
|
||||||
|
|
||||||
|
return sourceFactory.createMediaSource(mediaItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun httpGet(url: String?): String? {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val request = URL(url)
|
||||||
|
val conn = request.openConnection() as HttpURLConnection
|
||||||
|
|
||||||
|
return@withContext conn.getHeaderField("Content-Type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
hlsMediaSource = HlsMediaSource.Factory(dataSourceFactory)
|
||||||
|
progressiveMediaSource = ProgressiveMediaSource.Factory(dataSourceFactory, DefaultExtractorsFactory())
|
||||||
|
|
||||||
|
loadErrorHandlingPolicy = DefaultLoadErrorHandlingPolicy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -429,6 +429,7 @@ public final class PreferenceUtil {
|
||||||
defaultCategories.add(new CategoryInfo(CategoryInfo.Category.ARTISTS, true));
|
defaultCategories.add(new CategoryInfo(CategoryInfo.Category.ARTISTS, true));
|
||||||
defaultCategories.add(new CategoryInfo(CategoryInfo.Category.GENRES, true));
|
defaultCategories.add(new CategoryInfo(CategoryInfo.Category.GENRES, true));
|
||||||
defaultCategories.add(new CategoryInfo(CategoryInfo.Category.PLAYLISTS, true));
|
defaultCategories.add(new CategoryInfo(CategoryInfo.Category.PLAYLISTS, true));
|
||||||
|
defaultCategories.add(new CategoryInfo(CategoryInfo.Category.FAVORITES, true));
|
||||||
return defaultCategories;
|
return defaultCategories;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
<string name="genres">Genres</string>
|
<string name="genres">Genres</string>
|
||||||
<string name="songs">Songs</string>
|
<string name="songs">Songs</string>
|
||||||
<string name="playlists">Playlists</string>
|
<string name="playlists">Playlists</string>
|
||||||
|
<string name="favorites">Favorites</string>
|
||||||
<string name="no_playlists">No playlists</string>
|
<string name="no_playlists">No playlists</string>
|
||||||
<string name="no_albums">No albums</string>
|
<string name="no_albums">No albums</string>
|
||||||
<string name="no_songs">No songs</string>
|
<string name="no_songs">No songs</string>
|
||||||
|
|
|
||||||
8
metadata/en-US/changelogs/9.txt
Normal file
8
metadata/en-US/changelogs/9.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
- move glide cache to the proper location
|
||||||
|
- fix issue with lock screen cover display
|
||||||
|
- change default sort method to random
|
||||||
|
- replace queue store with room database
|
||||||
|
- huge improvements for login activity style
|
||||||
|
- add crash activity to display errors
|
||||||
|
- refactor ExoPlayer wrapper
|
||||||
|
- implement favorites tab on home activity
|
||||||
Loading…
Add table
Add a link
Reference in a new issue