Merge pull request #6 from adrianvic/feature-download-playback
Fix metadata and add downloads tab with listing logic and UI updates
|
|
@ -0,0 +1,69 @@
|
||||||
|
package org.adrianvictor.geleia.adapter;
|
||||||
|
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.adrianvictor.geleia.R;
|
||||||
|
import org.adrianvictor.geleia.model.Song;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class DownloadsAdapter extends RecyclerView.Adapter<DownloadsAdapter.ViewHolder> {
|
||||||
|
private final List<Song> mSongs;
|
||||||
|
private final int mLayoutId;
|
||||||
|
|
||||||
|
public DownloadsAdapter(int layoutId) {
|
||||||
|
mLayoutId = layoutId;
|
||||||
|
this.mSongs = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void swapDataSet(List<Song> newSongs) {
|
||||||
|
mSongs.clear();
|
||||||
|
if (newSongs != null) {
|
||||||
|
mSongs.addAll(newSongs);
|
||||||
|
}
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public DownloadsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext()).inflate(mLayoutId, parent, false);
|
||||||
|
return new ViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull DownloadsAdapter.ViewHolder holder, int position) {
|
||||||
|
final Song song = mSongs.get(position);
|
||||||
|
|
||||||
|
holder.title.setText(song.title);
|
||||||
|
holder.artist.setText(song.artistName);
|
||||||
|
|
||||||
|
// TODO: Load album cover into holder.cover using Glide
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return mSongs.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
public final TextView title;
|
||||||
|
public final TextView artist;
|
||||||
|
public final ImageView cover;
|
||||||
|
|
||||||
|
public ViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
title = itemView.findViewById(R.id.title);
|
||||||
|
artist = itemView.findViewById(R.id.text);
|
||||||
|
cover = itemView.findViewById(R.id.image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,9 @@ 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 org.adrianvictor.geleia.activities.UnreachableActivity;
|
||||||
|
import org.adrianvictor.geleia.fragments.OfflineFragment;
|
||||||
|
import org.adrianvictor.geleia.fragments.library.DownloadsFragment;
|
||||||
import org.adrianvictor.geleia.fragments.library.FavoritesFragment;
|
import org.adrianvictor.geleia.fragments.library.FavoritesFragment;
|
||||||
import org.adrianvictor.geleia.model.Category;
|
import org.adrianvictor.geleia.model.Category;
|
||||||
import org.adrianvictor.geleia.fragments.library.AlbumsFragment;
|
import org.adrianvictor.geleia.fragments.library.AlbumsFragment;
|
||||||
|
|
@ -157,7 +160,8 @@ public class MusicLibraryPagerAdapter extends FragmentPagerAdapter {
|
||||||
ARTISTS(ArtistsFragment.class),
|
ARTISTS(ArtistsFragment.class),
|
||||||
GENRES(GenresFragment.class),
|
GENRES(GenresFragment.class),
|
||||||
PLAYLISTS(PlaylistsFragment.class),
|
PLAYLISTS(PlaylistsFragment.class),
|
||||||
FAVORITES(FavoritesFragment.class);
|
FAVORITES(FavoritesFragment.class),
|
||||||
|
DOWNLOADS(DownloadsFragment.class);
|
||||||
|
|
||||||
private final Class<? extends Fragment> mFragmentClass;
|
private final Class<? extends Fragment> mFragmentClass;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ public interface CacheDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
void insertCache(Cache cache);
|
void insertCache(Cache cache);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM cache")
|
||||||
|
List<Cache> getAll();
|
||||||
|
|
||||||
@Query("SELECT * FROM songs LEFT JOIN cache USING(id) WHERE songs.id IN (:ids)")
|
@Query("SELECT * FROM songs LEFT JOIN cache USING(id) WHERE songs.id IN (:ids)")
|
||||||
List<Song> getSongs(List<String> ids);
|
List<Song> getSongs(List<String> ids);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
package org.adrianvictor.geleia.fragments.library;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
|
|
||||||
|
import org.adrianvictor.geleia.App;
|
||||||
|
import org.adrianvictor.geleia.adapter.DownloadsAdapter;
|
||||||
|
|
||||||
|
import org.adrianvictor.geleia.database.Cache;
|
||||||
|
import org.adrianvictor.geleia.model.Song;
|
||||||
|
import org.adrianvictor.geleia.model.SortMethod;
|
||||||
|
import org.adrianvictor.geleia.model.SortOrder;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class DownloadsFragment extends AbsLibraryPagerRecyclerViewCustomGridSizeFragment<DownloadsAdapter, GridLayoutManager, Void> {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
protected DownloadsAdapter createAdapter() {
|
||||||
|
return new DownloadsAdapter(getItemLayoutRes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
protected GridLayoutManager createLayoutManager() {
|
||||||
|
return new GridLayoutManager(getActivity(), getGridSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
protected Void createQuery() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void loadItems(int index) {
|
||||||
|
new Thread(() -> {
|
||||||
|
List<Cache> cachedEntries = App.getDatabase().cacheDao().getAll();
|
||||||
|
|
||||||
|
List<String> songIds = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Cache entry : cachedEntries) {
|
||||||
|
songIds.add(entry.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Song> downloadedSongs = App.getDatabase().cacheDao().getSongs(songIds);
|
||||||
|
|
||||||
|
if (getActivity() != null) {
|
||||||
|
getActivity().runOnUiThread(() -> {
|
||||||
|
getAdapter().swapDataSet(downloadedSongs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
protected int loadGridSize() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void saveGridSize(int gridColumns) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int loadGridSizeLand() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void saveGridSizeLand(int gridColumns) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void saveUsePalette(boolean usePalette) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean loadUsePalette() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setUsePalette(boolean usePalette) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setGridSize(int gridSize) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SortMethod loadSortMethod() {
|
||||||
|
return SortMethod.ADDED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void saveSortMethod(SortMethod sortMethod) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setSortMethod(SortMethod sortMethod) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SortOrder loadSortOrder() {
|
||||||
|
return SortOrder.ASCENDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void saveSortOrder(SortOrder sortOrder) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setSortOrder(SortOrder sortOrder) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,8 @@ public enum Category {
|
||||||
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);
|
FAVORITES(R.string.favorites),
|
||||||
|
DOWNLOADS(R.string.downloads);
|
||||||
|
|
||||||
@StringRes
|
@StringRes
|
||||||
public final int title;
|
public final int title;
|
||||||
|
|
|
||||||
13
app/src/main/res/layout/downloads_fragment.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context="org.adrianvictor.geleia.fragments.library.DownloadsFragment">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:layout_width="409dp"
|
||||||
|
android:layout_height="729dp"
|
||||||
|
tools:layout_editor_absoluteX="1dp"
|
||||||
|
tools:layout_editor_absoluteY="1dp" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
@ -247,6 +247,7 @@
|
||||||
|
|
||||||
<string name="download_channel_name">Downloads</string>
|
<string name="download_channel_name">Downloads</string>
|
||||||
<string name="downloading_songs">Downloading songs</string>
|
<string name="downloading_songs">Downloading songs</string>
|
||||||
|
<string name="downloads">Downloads</string>
|
||||||
<plurals name="downloading_s_songs">
|
<plurals name="downloading_s_songs">
|
||||||
<item quantity="one">Downloading %d song</item>
|
<item quantity="one">Downloading %d song</item>
|
||||||
<item quantity="other">Downloading %d songs</item>
|
<item quantity="other">Downloading %d songs</item>
|
||||||
|
|
|
||||||
|
|
@ -1 +1,16 @@
|
||||||
This is a native music player for Android devices that connects to Jellyfin media servers. The code is based on Gelli's archived repository, which is based on an old version of Phonograph. Jamfish is made for personal use, but contributions are welcome! Please open an issue to discuss larger changes before submitting a pull request.
|
This is a native music player for Android devices that connects to Jellyfin media servers. The code is based on Gelli's archived repository, which is based on an old version of Phonograph. Jamfish is made for personal use, but contributions are welcome! Please open an issue to discuss larger changes before submitting a pull request.
|
||||||
|
|
||||||
|
Features
|
||||||
|
- Basic library navigation
|
||||||
|
- Download songs to internal storage individually or through batch actions
|
||||||
|
- Gapless playback
|
||||||
|
- Sort albums and songs by different fields
|
||||||
|
- Search media for partial matches
|
||||||
|
- Media service integration with notification
|
||||||
|
- Favorites and playlists
|
||||||
|
- Playback history reporting
|
||||||
|
- Filter content by library
|
||||||
|
|
||||||
|
Requisites:
|
||||||
|
- A Jellfin server
|
||||||
|
- Android 4.4 or later
|
||||||
|
Before Width: | Height: | Size: 650 KiB After Width: | Height: | Size: 650 KiB |
|
Before Width: | Height: | Size: 340 KiB After Width: | Height: | Size: 340 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |