diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..603b1407
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 00000000..26d33521
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 00000000..61a9130c
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 00000000..b617266a
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 00000000..eb2873e7
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 00000000..d5d35ec4
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..35eb1ddf
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 00000000..f47d0b27
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,63 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+
+
+android {
+ compileSdkVersion 30
+ buildToolsVersion "29.0.3"
+
+ defaultConfig {
+ applicationId "com.cappielloantonio.play"
+ minSdkVersion 26
+ targetSdkVersion 30
+ versionCode 1
+ versionName "1.1"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility 1.8
+ targetCompatibility 1.8
+ }
+
+ buildFeatures {
+ viewBinding = true
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: "libs", include: ["*.jar"])
+ implementation 'androidx.core:core-ktx:1.3.2'
+ implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.4.10'
+
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+ implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
+ implementation 'pub.devrel:easypermissions:3.0.0'
+ implementation 'androidx.preference:preference-ktx:1.1.1'
+ implementation 'com.android.volley:volley:1.1.1'
+ implementation "com.paulrybitskyi.persistentsearchview:persistentsearchview:1.1.3"
+ implementation 'com.google.android.material:material:1.2.1'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
+ implementation 'androidx.recyclerview:recyclerview:1.1.0'
+ implementation 'com.github.bumptech.glide:glide:4.11.0'
+ implementation "androidx.room:room-runtime:2.2.5"
+ implementation 'com.github.jellyfin.jellyfin-apiclient-java:android:0.7.7'
+ implementation "androidx.cardview:cardview:1.0.0"
+ annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
+ annotationProcessor "androidx.room:room-compiler:2.2.5"
+ testImplementation 'junit:junit:4.13.1'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.2'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+ debugImplementation 'com.amitshekhar.android:debug-db:1.0.6'
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/cappielloantonio/play/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/cappielloantonio/play/ExampleInstrumentedTest.java
new file mode 100644
index 00000000..12c9bc2e
--- /dev/null
+++ b/app/src/androidTest/java/com/cappielloantonio/play/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.cappielloantonio.play;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("com.cappielloantonio.libr", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..89e292aa
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/play/App.java b/app/src/main/java/com/cappielloantonio/play/App.java
new file mode 100644
index 00000000..80c65746
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/App.java
@@ -0,0 +1,65 @@
+package com.cappielloantonio.play;
+
+import android.app.Application;
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.preference.PreferenceManager;
+
+import com.android.volley.RequestQueue;
+import com.android.volley.toolbox.Volley;
+import com.cappielloantonio.play.helper.ThemeHelper;
+import com.cappielloantonio.play.util.PreferenceUtil;
+
+import org.jellyfin.apiclient.AppInfo;
+import org.jellyfin.apiclient.Jellyfin;
+import org.jellyfin.apiclient.JellyfinAndroidKt;
+import org.jellyfin.apiclient.JellyfinOptions;
+import org.jellyfin.apiclient.interaction.AndroidDevice;
+import org.jellyfin.apiclient.interaction.ApiClient;
+import org.jellyfin.apiclient.interaction.ApiEventListener;
+import org.jellyfin.apiclient.logging.NullLogger;
+
+public class App extends Application {
+ private static final String TAG = "App";
+ private static App instance;
+ private static ApiClient apiClient;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ ThemeHelper.applyTheme(PreferenceUtil.getInstance(getApplicationContext()).getTheme());
+ }
+
+ public static App getInstance() {
+ if (instance == null) {
+ instance = new App();
+ }
+ return instance;
+ }
+
+ public static ApiClient getApiClientInstance(Context context) {
+ if (apiClient == null) {
+ apiClient = getApiClient(context);
+ }
+ return apiClient;
+ }
+
+ public RequestQueue getRequestQueue(Context context) {
+ return Volley.newRequestQueue(context);
+ }
+
+ private static ApiClient getApiClient(Context context) {
+ String server = PreferenceUtil.getInstance(context).getServer();
+
+ JellyfinOptions.Builder options = new JellyfinOptions.Builder();
+ options.setLogger(new NullLogger());
+ options.setAppInfo(new AppInfo(context.getString(R.string.app_name), BuildConfig.VERSION_NAME));
+ JellyfinAndroidKt.android(options, context);
+
+ Jellyfin jellyfin = new Jellyfin(options.build());
+
+ return jellyfin.createApi(server, null, AndroidDevice.fromContext(context), new ApiEventListener());
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/adapter/AlbumAdapter.java b/app/src/main/java/com/cappielloantonio/play/adapter/AlbumAdapter.java
new file mode 100644
index 00000000..1ef23c54
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/adapter/AlbumAdapter.java
@@ -0,0 +1,80 @@
+package com.cappielloantonio.play.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.cappielloantonio.play.R;
+import com.cappielloantonio.play.model.Album;
+import com.cappielloantonio.play.model.Genre;
+
+import java.util.List;
+
+public class AlbumAdapter extends RecyclerView.Adapter {
+ private static final String TAG = "RecentMusicAdapter";
+ private List albums;
+ private LayoutInflater mInflater;
+ private Context context;
+ private ItemClickListener itemClickListener;
+
+ public AlbumAdapter(Context context, List albums) {
+ this.context = context;
+ this.mInflater = LayoutInflater.from(context);
+ this.albums = albums;
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = mInflater.inflate(R.layout.item_library_album, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ Album album = albums.get(position);
+
+ holder.textAlbumName.setText(album.getTitle());
+ holder.textArtistName.setText(album.getArtistName());
+ }
+
+ @Override
+ public int getItemCount() {
+ return albums.size();
+ }
+
+ public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
+ TextView textAlbumName;
+ TextView textArtistName;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+
+ textAlbumName = itemView.findViewById(R.id.album_name_label);
+ textArtistName = itemView.findViewById(R.id.artist_name_label);
+
+ itemView.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (itemClickListener != null) itemClickListener.onItemClick(view, getAdapterPosition());
+ }
+ }
+
+ public void setItems(List albums) {
+ this.albums = albums;
+ notifyDataSetChanged();
+ }
+
+ public void setClickListener(ItemClickListener itemClickListener) {
+ this.itemClickListener = itemClickListener;
+ }
+
+ public interface ItemClickListener {
+ void onItemClick(View view, int position);
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/adapter/ArtistAdapter.java b/app/src/main/java/com/cappielloantonio/play/adapter/ArtistAdapter.java
new file mode 100644
index 00000000..72e28376
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/adapter/ArtistAdapter.java
@@ -0,0 +1,77 @@
+package com.cappielloantonio.play.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.cappielloantonio.play.R;
+import com.cappielloantonio.play.model.Album;
+import com.cappielloantonio.play.model.Artist;
+
+import java.util.List;
+
+public class ArtistAdapter extends RecyclerView.Adapter {
+ private static final String TAG = "ArtistAdapter";
+ private List artists;
+ private LayoutInflater mInflater;
+ private Context context;
+ private ItemClickListener itemClickListener;
+
+ public ArtistAdapter(Context context, List artists) {
+ this.context = context;
+ this.mInflater = LayoutInflater.from(context);
+ this.artists = artists;
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = mInflater.inflate(R.layout.item_library_artist, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ Artist artist = artists.get(position);
+
+ holder.textArtistName.setText(artist.getName());
+ }
+
+ @Override
+ public int getItemCount() {
+ return artists.size();
+ }
+
+ public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
+ TextView textArtistName;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+
+ textArtistName = itemView.findViewById(R.id.artist_name_label);
+
+ itemView.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (itemClickListener != null) itemClickListener.onItemClick(view, getAdapterPosition());
+ }
+ }
+
+ public void setItems(List artists) {
+ this.artists = artists;
+ notifyDataSetChanged();
+ }
+
+ public void setClickListener(ItemClickListener itemClickListener) {
+ this.itemClickListener = itemClickListener;
+ }
+
+ public interface ItemClickListener {
+ void onItemClick(View view, int position);
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/adapter/DiscoverSongAdapter.java b/app/src/main/java/com/cappielloantonio/play/adapter/DiscoverSongAdapter.java
new file mode 100644
index 00000000..98df9a48
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/adapter/DiscoverSongAdapter.java
@@ -0,0 +1,64 @@
+package com.cappielloantonio.play.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.viewpager.widget.PagerAdapter;
+
+import com.cappielloantonio.play.R;
+import com.cappielloantonio.play.model.Song;
+
+import java.util.List;
+
+public class DiscoverSongAdapter extends PagerAdapter {
+
+ private List songs;
+ private LayoutInflater layoutInflater;
+ private Context context;
+
+ public DiscoverSongAdapter(Context context, List models) {
+ this.context = context;
+ this.songs = models;
+ }
+
+ @Override
+ public int getCount() {
+ return songs.size();
+ }
+
+ @Override
+ public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
+ return view.equals(object);
+ }
+
+ @NonNull
+ @Override
+ public Object instantiateItem(@NonNull ViewGroup container, final int position) {
+ layoutInflater = LayoutInflater.from(context);
+ View view = layoutInflater.inflate(R.layout.item_discover_song, container, false);
+
+ TextView title;
+ TextView desc;
+
+ title = view.findViewById(R.id.title_discover_song_label);
+ desc = view.findViewById(R.id.artist_discover_song_label);
+
+ title.setText(songs.get(position).getTitle());
+ desc.setText(songs.get(position).getAlbumName());
+
+ view.setOnClickListener(v -> Toast.makeText(context, songs.get(position).getTitle(), Toast.LENGTH_SHORT).show());
+
+ container.addView(view, 0);
+ return view;
+ }
+
+ @Override
+ public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
+ container.removeView((View)object);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/play/adapter/GenreAdapter.java b/app/src/main/java/com/cappielloantonio/play/adapter/GenreAdapter.java
new file mode 100644
index 00000000..50a0366a
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/adapter/GenreAdapter.java
@@ -0,0 +1,77 @@
+package com.cappielloantonio.play.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.cappielloantonio.play.R;
+import com.cappielloantonio.play.model.Genre;
+
+import java.util.List;
+
+public class GenreAdapter extends RecyclerView.Adapter {
+ private static final String TAG = "GenreAdapter";
+ private List genres;
+ private LayoutInflater mInflater;
+ private Context context;
+ private ItemClickListener itemClickListener;
+
+ public GenreAdapter(Context context, List genres) {
+ this.context = context;
+ this.mInflater = LayoutInflater.from(context);
+ this.genres = genres;
+ }
+
+ // inflates the row layout from xml when needed
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = mInflater.inflate(R.layout.item_library_genre, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ Genre genre = genres.get(position);
+
+ holder.textGenre.setText(genre.getName());
+ }
+
+ @Override
+ public int getItemCount() {
+ return genres.size();
+ }
+
+ public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
+ TextView textGenre;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+
+ textGenre = itemView.findViewById(R.id.genre_label);
+
+ itemView.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (itemClickListener != null) itemClickListener.onItemClick(view, getAdapterPosition());
+ }
+ }
+
+ public void setItems(List genres) {
+ this.genres = genres;
+ notifyDataSetChanged();
+ }
+
+ public void setClickListener(ItemClickListener itemClickListener) {
+ this.itemClickListener = itemClickListener;
+ }
+
+ public interface ItemClickListener {
+ void onItemClick(View view, int position);
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/adapter/PlaylistAdapter.java b/app/src/main/java/com/cappielloantonio/play/adapter/PlaylistAdapter.java
new file mode 100644
index 00000000..c2381ab4
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/adapter/PlaylistAdapter.java
@@ -0,0 +1,77 @@
+package com.cappielloantonio.play.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.cappielloantonio.play.R;
+import com.cappielloantonio.play.model.Artist;
+import com.cappielloantonio.play.model.Playlist;
+
+import java.util.List;
+
+public class PlaylistAdapter extends RecyclerView.Adapter {
+ private static final String TAG = "PlaylistAdapter";
+ private List playlists;
+ private LayoutInflater mInflater;
+ private Context context;
+ private ItemClickListener itemClickListener;
+
+ public PlaylistAdapter(Context context, List playlists) {
+ this.context = context;
+ this.mInflater = LayoutInflater.from(context);
+ this.playlists = playlists;
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = mInflater.inflate(R.layout.item_library_playlist, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ Playlist playlist = playlists.get(position);
+
+ holder.textPlaylistName.setText(playlist.getName());
+ }
+
+ @Override
+ public int getItemCount() {
+ return playlists.size();
+ }
+
+ public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
+ TextView textPlaylistName;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+
+ textPlaylistName = itemView.findViewById(R.id.playlist_name_text);
+
+ itemView.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (itemClickListener != null) itemClickListener.onItemClick(view, getAdapterPosition());
+ }
+ }
+
+ public void setItems(List playlists) {
+ this.playlists = playlists;
+ notifyDataSetChanged();
+ }
+
+ public void setClickListener(ItemClickListener itemClickListener) {
+ this.itemClickListener = itemClickListener;
+ }
+
+ public interface ItemClickListener {
+ void onItemClick(View view, int position);
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/adapter/RecentMusicAdapter.java b/app/src/main/java/com/cappielloantonio/play/adapter/RecentMusicAdapter.java
new file mode 100644
index 00000000..6b4958d2
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/adapter/RecentMusicAdapter.java
@@ -0,0 +1,76 @@
+package com.cappielloantonio.play.adapter;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.cappielloantonio.play.R;
+import com.cappielloantonio.play.model.Song;
+
+import java.util.List;
+
+public class RecentMusicAdapter extends RecyclerView.Adapter {
+ private static final String TAG = "RecentMusicAdapter";
+ private List songs;
+ private LayoutInflater mInflater;
+ private Context context;
+ private ItemClickListener itemClickListener;
+
+ public RecentMusicAdapter(Context context, List songs) {
+ this.context = context;
+ this.mInflater = LayoutInflater.from(context);
+ this.songs = songs;
+ }
+
+ // inflates the row layout from xml when needed
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = mInflater.inflate(R.layout.item_recent_track, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ Song song = songs.get(position);
+
+ holder.textTitle.setText(song.getTitle());
+ holder.textArtist.setText(song.getAlbumName());
+ }
+
+ @Override
+ public int getItemCount() {
+ return songs.size();
+ }
+
+ public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
+ TextView textTitle;
+ TextView textArtist;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+
+ textTitle = itemView.findViewById(R.id.title_track_label);
+ textArtist = itemView.findViewById(R.id.artist_track_label);
+
+ itemView.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (itemClickListener != null) itemClickListener.onItemClick(view, getAdapterPosition());
+ }
+ }
+
+ public void setClickListener(ItemClickListener itemClickListener) {
+ this.itemClickListener = itemClickListener;
+ }
+
+ public interface ItemClickListener {
+ void onItemClick(View view, int position);
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/database/AppDatabase.java b/app/src/main/java/com/cappielloantonio/play/database/AppDatabase.java
new file mode 100644
index 00000000..5221a51a
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/database/AppDatabase.java
@@ -0,0 +1,46 @@
+package com.cappielloantonio.play.database;
+
+import android.content.Context;
+
+import androidx.room.Database;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+
+import com.cappielloantonio.play.database.dao.AlbumDao;
+import com.cappielloantonio.play.database.dao.ArtistDao;
+import com.cappielloantonio.play.database.dao.GenreDao;
+import com.cappielloantonio.play.database.dao.PlaylistDao;
+import com.cappielloantonio.play.database.dao.SongDao;
+import com.cappielloantonio.play.model.Album;
+import com.cappielloantonio.play.model.Artist;
+import com.cappielloantonio.play.model.Genre;
+import com.cappielloantonio.play.model.Playlist;
+import com.cappielloantonio.play.model.Song;
+
+@Database(entities = {Album.class, Artist.class, Genre.class, Playlist.class, Song.class}, version = 2, exportSchema = false)
+public abstract class AppDatabase extends RoomDatabase {
+ private static final String TAG = "AppDatabase";
+
+ private static AppDatabase instance;
+ private final static String DB_NAME = "play_db";
+
+ public static synchronized AppDatabase getInstance(Context context) {
+
+ if (instance == null) {
+ instance = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DB_NAME)
+ .fallbackToDestructiveMigration()
+ .build();
+ }
+ return instance;
+ }
+
+ public abstract AlbumDao albumDao();
+
+ public abstract ArtistDao artistDao();
+
+ public abstract GenreDao genreDao();
+
+ public abstract PlaylistDao playlistDao();
+
+ public abstract SongDao songDao();
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/database/dao/AlbumDao.java b/app/src/main/java/com/cappielloantonio/play/database/dao/AlbumDao.java
new file mode 100644
index 00000000..6d6fe712
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/database/dao/AlbumDao.java
@@ -0,0 +1,31 @@
+package com.cappielloantonio.play.database.dao;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+
+import com.cappielloantonio.play.model.Album;
+import com.cappielloantonio.play.model.Song;
+
+import java.util.List;
+
+@Dao
+public interface AlbumDao {
+ @Query("SELECT * FROM album")
+ LiveData> getAll();
+
+ @Query("SELECT EXISTS(SELECT * FROM album WHERE id = :id)")
+ boolean exist(String id);
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insert(Album album);
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insertAll(List albums);
+
+ @Delete
+ void delete(Album album);
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/play/database/dao/ArtistDao.java b/app/src/main/java/com/cappielloantonio/play/database/dao/ArtistDao.java
new file mode 100644
index 00000000..10a847fa
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/database/dao/ArtistDao.java
@@ -0,0 +1,31 @@
+package com.cappielloantonio.play.database.dao;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+
+import com.cappielloantonio.play.model.Album;
+import com.cappielloantonio.play.model.Artist;
+
+import java.util.List;
+
+@Dao
+public interface ArtistDao {
+ @Query("SELECT * FROM artist")
+ LiveData> getAll();
+
+ @Query("SELECT EXISTS(SELECT * FROM artist WHERE id = :id)")
+ boolean exist(String id);
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insert(Artist artist);
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insertAll(List artists);
+
+ @Delete
+ void delete(Artist artist);
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/play/database/dao/GenreDao.java b/app/src/main/java/com/cappielloantonio/play/database/dao/GenreDao.java
new file mode 100644
index 00000000..b9fba993
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/database/dao/GenreDao.java
@@ -0,0 +1,31 @@
+package com.cappielloantonio.play.database.dao;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+
+import com.cappielloantonio.play.model.Artist;
+import com.cappielloantonio.play.model.Genre;
+
+import java.util.List;
+
+@Dao
+public interface GenreDao {
+ @Query("SELECT * FROM genre")
+ LiveData> getAll();
+
+ @Query("SELECT EXISTS(SELECT * FROM genre WHERE id = :id)")
+ boolean exist(String id);
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insert(Genre genre);
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insertAll(List genres);
+
+ @Delete
+ void delete(Genre genre);
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/play/database/dao/PlaylistDao.java b/app/src/main/java/com/cappielloantonio/play/database/dao/PlaylistDao.java
new file mode 100644
index 00000000..499ca1e9
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/database/dao/PlaylistDao.java
@@ -0,0 +1,31 @@
+package com.cappielloantonio.play.database.dao;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+
+import com.cappielloantonio.play.model.Artist;
+import com.cappielloantonio.play.model.Playlist;
+
+import java.util.List;
+
+@Dao
+public interface PlaylistDao {
+ @Query("SELECT * FROM playlist")
+ LiveData> getAll();
+
+ @Query("SELECT EXISTS(SELECT * FROM playlist WHERE id = :id)")
+ boolean exist(String id);
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insert(Playlist playlist);
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insertAll(List playlists);
+
+ @Delete
+ void delete(Playlist playlist);
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/play/database/dao/SongDao.java b/app/src/main/java/com/cappielloantonio/play/database/dao/SongDao.java
new file mode 100644
index 00000000..038a1809
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/database/dao/SongDao.java
@@ -0,0 +1,30 @@
+package com.cappielloantonio.play.database.dao;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+
+import com.cappielloantonio.play.model.Song;
+
+import java.util.List;
+
+@Dao
+public interface SongDao {
+ @Query("SELECT * FROM song")
+ LiveData> getAll();
+
+ @Query("SELECT EXISTS(SELECT * FROM song WHERE id = :id)")
+ boolean exist(String id);
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insert(Song song);
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insertAll(List songs);
+
+ @Delete
+ void delete(Song song);
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/play/glide/CustomGlideModule.java b/app/src/main/java/com/cappielloantonio/play/glide/CustomGlideModule.java
new file mode 100644
index 00000000..83715018
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/glide/CustomGlideModule.java
@@ -0,0 +1,27 @@
+package com.cappielloantonio.play.glide;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import com.bumptech.glide.GlideBuilder;
+import com.bumptech.glide.annotation.GlideModule;
+import com.bumptech.glide.load.DecodeFormat;
+import com.bumptech.glide.load.engine.cache.DiskLruCacheFactory;
+import com.bumptech.glide.module.AppGlideModule;
+import com.bumptech.glide.request.RequestOptions;
+import com.cappielloantonio.play.util.PreferenceUtil;
+
+import java.io.File;
+
+@GlideModule
+public class CustomGlideModule extends AppGlideModule {
+ @Override
+ public void applyOptions(@NonNull Context context, GlideBuilder builder) {
+ File file = new File(context.getCacheDir() + "glide");
+ int size = PreferenceUtil.getInstance(context).getImageCacheSize();
+
+ builder.setDiskCache(new DiskLruCacheFactory(() -> file, size));
+ builder.setDefaultRequestOptions(new RequestOptions().format(DecodeFormat.PREFER_RGB_565));
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/helper/EventListenerHelper.java b/app/src/main/java/com/cappielloantonio/play/helper/EventListenerHelper.java
new file mode 100644
index 00000000..0c9ec0f9
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/helper/EventListenerHelper.java
@@ -0,0 +1,111 @@
+package com.cappielloantonio.play.helper;
+
+import android.util.Log;
+
+import org.jellyfin.apiclient.interaction.ApiClient;
+import org.jellyfin.apiclient.interaction.ApiEventListener;
+import org.jellyfin.apiclient.model.apiclient.RemoteLogoutReason;
+import org.jellyfin.apiclient.model.apiclient.SessionUpdatesEventArgs;
+import org.jellyfin.apiclient.model.dto.UserDto;
+import org.jellyfin.apiclient.model.entities.LibraryUpdateInfo;
+import org.jellyfin.apiclient.model.session.BrowseRequest;
+import org.jellyfin.apiclient.model.session.GeneralCommand;
+import org.jellyfin.apiclient.model.session.MessageCommand;
+import org.jellyfin.apiclient.model.session.PlayRequest;
+import org.jellyfin.apiclient.model.session.PlaystateRequest;
+import org.jellyfin.apiclient.model.session.SessionInfoDto;
+import org.jellyfin.apiclient.model.session.UserDataChangeInfo;
+
+public class EventListenerHelper extends ApiEventListener {
+ private static final String TAG = "EventListenerHelper";
+
+ @Override
+ public void onRemoteLoggedOut(ApiClient client, RemoteLogoutReason reason) {
+ Log.i(TAG, "onRemoteLoggedOut: " + reason);
+ }
+
+ @Override
+ public void onUserUpdated(ApiClient client, UserDto userDto) {
+ Log.i(TAG, "onUserUpdated: " + userDto.getName());
+ }
+
+ @Override
+ public void onLibraryChanged(ApiClient client, LibraryUpdateInfo info) {
+ Log.i(TAG, "onLibraryChanged");
+ }
+
+ @Override
+ public void onUserConfigurationUpdated(ApiClient client, UserDto userDto) {
+ Log.i(TAG, "onUserConfigurationUpdated");
+ }
+
+ @Override
+ public void onBrowseCommand(ApiClient client, BrowseRequest command) {
+ Log.i(TAG, "onBrowseCommand: " + command.getItemName());
+ }
+
+ @Override
+ public void onPlayCommand(ApiClient client, PlayRequest command) {
+ Log.i(TAG, "onPlayCommand: " + command.getPlayCommand());
+ }
+
+ @Override
+ public void onPlaystateCommand(ApiClient client, PlaystateRequest command) {
+ Log.i(TAG, "onPlayStateCommand");
+ }
+
+ @Override
+ public void onMessageCommand(ApiClient client, MessageCommand command) {
+ Log.i(TAG, "onMessageCommand");
+ }
+
+ @Override
+ public void onGeneralCommand(ApiClient client, GeneralCommand command) {
+ Log.i(TAG, "onGeneralCommand: " + command.getName());
+ }
+
+ @Override
+ public void onSendStringCommand(ApiClient client, String value) {
+ Log.i(TAG, "onSendStringCommand");
+ }
+
+ @Override
+ public void onSetVolumeCommand(ApiClient client, int value) {
+ Log.i(TAG, "onSetVolumeCommand");
+ }
+
+ @Override
+ public void onSetAudioStreamIndexCommand(ApiClient client, int value) {
+ Log.i(TAG, "onSetAudioStreamIndexCommand");
+ }
+
+ @Override
+ public void onSetSubtitleStreamIndexCommand(ApiClient client, int value) {
+ Log.i(TAG, "onSetSubtitleStreamIndexCommand");
+ }
+
+ @Override
+ public void onUserDataChanged(ApiClient client, UserDataChangeInfo info) {
+ Log.i(TAG, "onUserDataChanged");
+ }
+
+ @Override
+ public void onSessionsUpdated(ApiClient client, SessionUpdatesEventArgs args) {
+ Log.i(TAG, "onSessionsUpdated");
+ }
+
+ @Override
+ public void onPlaybackStart(ApiClient client, SessionInfoDto info) {
+ Log.i(TAG, "onPlaybackStart");
+ }
+
+ @Override
+ public void onPlaybackStopped(ApiClient client, SessionInfoDto info) {
+ Log.i(TAG, "onPlaybackStopped");
+ }
+
+ @Override
+ public void onSessionEnded(ApiClient client, SessionInfoDto info) {
+ Log.i(TAG, "onSessionEnded");
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/play/helper/ThemeHelper.java b/app/src/main/java/com/cappielloantonio/play/helper/ThemeHelper.java
new file mode 100644
index 00000000..0ffd264f
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/helper/ThemeHelper.java
@@ -0,0 +1,33 @@
+package com.cappielloantonio.play.helper;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatDelegate;
+
+public class ThemeHelper {
+ public static final String LIGHT_MODE = "light";
+ public static final String DARK_MODE = "dark";
+ public static final String DEFAULT_MODE = "default";
+
+ public static void applyTheme(@NonNull String themePref) {
+ switch (themePref) {
+ case LIGHT_MODE: {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
+ break;
+ }
+ case DARK_MODE: {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
+ break;
+ }
+ default: {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
+ } else {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
+ }
+ break;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/play/interfaces/MediaCallback.java b/app/src/main/java/com/cappielloantonio/play/interfaces/MediaCallback.java
new file mode 100644
index 00000000..67417917
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/interfaces/MediaCallback.java
@@ -0,0 +1,10 @@
+package com.cappielloantonio.play.interfaces;
+
+import com.android.volley.VolleyError;
+
+import java.util.List;
+
+public interface MediaCallback {
+ void onError(Exception exception);
+ void onLoadMedia(List> media);
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/play/model/Album.java b/app/src/main/java/com/cappielloantonio/play/model/Album.java
new file mode 100644
index 00000000..3c30019d
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/model/Album.java
@@ -0,0 +1,193 @@
+package com.cappielloantonio.play.model;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.Ignore;
+import androidx.room.PrimaryKey;
+
+import org.jellyfin.apiclient.model.dto.BaseItemDto;
+import org.jellyfin.apiclient.model.entities.ImageType;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@Entity(tableName = "album")
+public class Album implements Parcelable {
+ @Ignore
+ public List songs;
+
+ @NonNull
+ @PrimaryKey
+ @ColumnInfo(name = "id")
+ public String id;
+
+ @ColumnInfo(name = "title")
+ public String title;
+
+ @ColumnInfo(name = "year")
+ public int year;
+
+ @ColumnInfo(name = "artistId")
+ public String artistId;
+
+ @ColumnInfo(name = "artistName")
+ public String artistName;
+
+ @ColumnInfo(name = "primary")
+ public String primary;
+
+ @ColumnInfo(name = "blurHash")
+ public String blurHash;
+
+ public Album(@NonNull String id, String title, int year, String artistId, String artistName, String primary, String blurHash) {
+ this.id = id;
+ this.title = title;
+ this.year = year;
+ this.artistId = artistId;
+ this.artistName = artistName;
+ this.primary = primary;
+ this.blurHash = blurHash;
+ }
+
+ @Ignore
+ public Album(BaseItemDto itemDto) {
+ this.id = itemDto.getId();
+ this.title = itemDto.getName();
+ this.year = itemDto.getProductionYear() != null ? itemDto.getProductionYear() : 0;
+
+ if (itemDto.getAlbumArtists().size() != 0) {
+ this.artistId = itemDto.getAlbumArtists().get(0).getId();
+ this.artistName = itemDto.getAlbumArtists().get(0).getName();
+ } else if (itemDto.getArtistItems().size() != 0) {
+ this.artistId = itemDto.getArtistItems().get(0).getId();
+ this.artistName = itemDto.getArtistItems().get(0).getName();
+ }
+
+ this.primary = itemDto.getImageTags().containsKey(ImageType.Primary) ? id : null;
+ if (itemDto.getImageBlurHashes() != null && itemDto.getImageBlurHashes().get(ImageType.Primary) != null) {
+ this.blurHash = (String) itemDto.getImageBlurHashes().get(ImageType.Primary).values().toArray()[0];
+ }
+
+ this.songs = new ArrayList<>();
+ }
+
+ @NonNull
+ public String getId() {
+ return id;
+ }
+
+ public void setId(@NonNull String id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public int getYear() {
+ return year;
+ }
+
+ public void setYear(int year) {
+ this.year = year;
+ }
+
+ public String getArtistId() {
+ return artistId;
+ }
+
+ public void setArtistId(String artistId) {
+ this.artistId = artistId;
+ }
+
+ public String getArtistName() {
+ return artistName;
+ }
+
+ public void setArtistName(String artistName) {
+ this.artistName = artistName;
+ }
+
+ public String getPrimary() {
+ return primary;
+ }
+
+ public void setPrimary(String primary) {
+ this.primary = primary;
+ }
+
+ public String getBlurHash() {
+ return blurHash;
+ }
+
+ public void setBlurHash(String blurHash) {
+ this.blurHash = blurHash;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Album album = (Album) o;
+ return id.equals(album.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return id;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeString(title);
+ dest.writeInt(year);
+ dest.writeString(artistId);
+ dest.writeString(artistName);
+ dest.writeString(primary);
+ dest.writeString(blurHash);
+ }
+
+ protected Album(Parcel in) {
+ this.songs = new ArrayList<>();
+
+ this.id = in.readString();
+ this.title = in.readString();
+ this.year = in.readInt();
+ this.artistId = in.readString();
+ this.artistName = in.readString();
+ this.primary = in.readString();
+ this.blurHash = in.readString();
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ public Album createFromParcel(Parcel source) {
+ return new Album(source);
+ }
+
+ public Album[] newArray(int size) {
+ return new Album[size];
+ }
+ };
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/model/Artist.java b/app/src/main/java/com/cappielloantonio/play/model/Artist.java
new file mode 100644
index 00000000..7c318c32
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/model/Artist.java
@@ -0,0 +1,157 @@
+package com.cappielloantonio.play.model;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.Ignore;
+import androidx.room.PrimaryKey;
+
+import org.jellyfin.apiclient.model.dto.BaseItemDto;
+import org.jellyfin.apiclient.model.dto.GenreDto;
+import org.jellyfin.apiclient.model.entities.ImageType;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Entity(tableName = "artist")
+public class Artist implements Parcelable {
+ @Ignore
+ public List genres;
+ @Ignore
+ public List albums;
+ @Ignore
+ public List songs;
+
+ @NonNull
+ @PrimaryKey
+ @ColumnInfo(name = "id")
+ public String id;
+
+ @ColumnInfo(name = "name")
+ public String name;
+
+ @ColumnInfo(name = "primary")
+ public String primary;
+
+ @ColumnInfo(name = "blurHash")
+ public String blurHash;
+
+ public Artist(@NonNull String id, String name, String primary, String blurHash) {
+ this.id = id;
+ this.name = name;
+ this.primary = primary;
+ this.blurHash = blurHash;
+ }
+
+ @Ignore
+ public Artist(BaseItemDto itemDto) {
+ this.id = itemDto.getId();
+ this.name = itemDto.getName();
+
+ this.primary = itemDto.getImageTags().containsKey(ImageType.Primary) ? id : null;
+ if (itemDto.getImageBlurHashes() != null && itemDto.getImageBlurHashes().get(ImageType.Primary) != null) {
+ this.blurHash = (String) itemDto.getImageBlurHashes().get(ImageType.Primary).values().toArray()[0];
+ }
+
+ this.genres = new ArrayList<>();
+ this.albums = new ArrayList<>();
+ this.songs = new ArrayList<>();
+
+ if (itemDto.getGenreItems() != null) {
+ for (GenreDto genre : itemDto.getGenreItems()) {
+ genres.add(new Genre(genre));
+ }
+ }
+ }
+
+ @NonNull
+ public String getId() {
+ return id;
+ }
+
+ public void setId(@NonNull String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getPrimary() {
+ return primary;
+ }
+
+ public void setPrimary(String primary) {
+ this.primary = primary;
+ }
+
+ public String getBlurHash() {
+ return blurHash;
+ }
+
+ public void setBlurHash(String blurHash) {
+ this.blurHash = blurHash;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Artist artist = (Artist) o;
+ return id.equals(artist.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return id;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeString(name);
+ dest.writeString(primary);
+ dest.writeString(blurHash);
+ }
+
+ protected Artist(Parcel in) {
+ this.genres = new ArrayList<>();
+ this.albums = new ArrayList<>();
+ this.songs = new ArrayList<>();
+ this.id = in.readString();
+ this.name = in.readString();
+ this.primary = in.readString();
+ this.blurHash = in.readString();
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ @Override
+ public Artist createFromParcel(Parcel source) {
+ return new Artist(source);
+ }
+
+ @Override
+ public Artist[] newArray(int size) {
+ return new Artist[size];
+ }
+ };
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/play/model/Genre.java b/app/src/main/java/com/cappielloantonio/play/model/Genre.java
new file mode 100644
index 00000000..6f73e83d
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/model/Genre.java
@@ -0,0 +1,140 @@
+package com.cappielloantonio.play.model;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.Ignore;
+import androidx.room.PrimaryKey;
+
+import org.jellyfin.apiclient.model.dto.BaseItemDto;
+import org.jellyfin.apiclient.model.dto.GenreDto;
+import org.jellyfin.apiclient.model.entities.ImageType;
+
+import java.util.UUID;
+
+@Entity(tableName = "genre")
+public class Genre implements Parcelable {
+ @NonNull
+ @PrimaryKey
+ @ColumnInfo(name = "id")
+ public String id;
+
+ @ColumnInfo(name = "name")
+ public String name;
+
+ @ColumnInfo(name = "songCount")
+ public int songCount;
+
+ @ColumnInfo(name = "primary")
+ public String primary;
+
+ @ColumnInfo(name = "blurHash")
+ public String blurHash;
+
+ public Genre(@NonNull String id, String name, int songCount, String primary, String blurHash) {
+ this.id = id;
+ this.name = name;
+ this.songCount = songCount;
+ this.primary = primary;
+ this.blurHash = blurHash;
+ }
+
+ @Ignore
+ public Genre(GenreDto genreDto) {
+ this.id = genreDto.getId();
+ this.name = genreDto.getName();
+ this.songCount = 0;
+ }
+
+ @Ignore
+ public Genre(BaseItemDto itemDto) {
+ this.id = itemDto.getId();
+ this.name = itemDto.getName();
+ this.songCount = itemDto.getSongCount() != null ? itemDto.getSongCount() : 0;
+
+ this.primary = itemDto.getImageTags().containsKey(ImageType.Primary) ? id : null;
+ if (itemDto.getImageBlurHashes() != null && itemDto.getImageBlurHashes().get(ImageType.Primary) != null) {
+ this.blurHash = (String) itemDto.getImageBlurHashes().get(ImageType.Primary).values().toArray()[0];
+ }
+ }
+
+ @NonNull
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getSongCount() {
+ return songCount;
+ }
+
+ public String getPrimary() {
+ return primary;
+ }
+
+ public void setPrimary(String primary) {
+ this.primary = primary;
+ }
+
+ public String getBlurHash() {
+ return blurHash;
+ }
+
+ public void setBlurHash(String blurHash) {
+ this.blurHash = blurHash;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Genre genre = (Genre) o;
+ return id.equals(genre.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return id;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(this.id);
+ dest.writeString(this.name);
+ dest.writeInt(this.songCount);
+ }
+
+ protected Genre(Parcel in) {
+ this.id = in.readString();
+ this.name = in.readString();
+ this.songCount = in.readInt();
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ public Genre createFromParcel(Parcel source) {
+ return new Genre(source);
+ }
+
+ public Genre[] newArray(int size) {
+ return new Genre[size];
+ }
+ };
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/model/Playlist.java b/app/src/main/java/com/cappielloantonio/play/model/Playlist.java
new file mode 100644
index 00000000..7314aee0
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/model/Playlist.java
@@ -0,0 +1,119 @@
+package com.cappielloantonio.play.model;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.Ignore;
+import androidx.room.PrimaryKey;
+
+import org.jellyfin.apiclient.model.dto.BaseItemDto;
+import org.jellyfin.apiclient.model.entities.ImageType;
+
+@Entity(tableName = "playlist")
+public class Playlist implements Parcelable {
+ @NonNull
+ @PrimaryKey
+ @ColumnInfo(name = "id")
+ public String id;
+
+ @ColumnInfo(name = "name")
+ public String name;
+
+ @ColumnInfo(name = "primary")
+ public String primary;
+
+ @ColumnInfo(name = "blurHash")
+ public String blurHash;
+
+ public Playlist(@NonNull String id, String name, String primary, String blurHash) {
+ this.id = id;
+ this.name = name;
+ this.primary = primary;
+ this.blurHash = blurHash;
+ }
+
+ @Ignore
+ public Playlist(BaseItemDto itemDto) {
+ this.id = itemDto.getId();
+ this.name = itemDto.getName();
+
+ this.primary = itemDto.getImageTags().containsKey(ImageType.Primary) ? id : null;
+ if (itemDto.getImageBlurHashes() != null && itemDto.getImageBlurHashes().get(ImageType.Primary) != null) {
+ this.blurHash = (String) itemDto.getImageBlurHashes().get(ImageType.Primary).values().toArray()[0];
+ }
+ }
+
+ @NonNull
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getPrimary() {
+ return primary;
+ }
+
+ public void setPrimary(String primary) {
+ this.primary = primary;
+ }
+
+ public String getBlurHash() {
+ return blurHash;
+ }
+
+ public void setBlurHash(String blurHash) {
+ this.blurHash = blurHash;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Playlist playlist = (Playlist) o;
+ return id.equals(playlist.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return id;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(this.id);
+ dest.writeString(this.name);
+ }
+
+ protected Playlist(Parcel in) {
+ this.id = in.readString();
+ this.name = in.readString();
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ public Playlist createFromParcel(Parcel source) {
+ return new Playlist(source);
+ }
+
+ public Playlist[] newArray(int size) {
+ return new Playlist[size];
+ }
+ };
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/play/model/Song.java b/app/src/main/java/com/cappielloantonio/play/model/Song.java
new file mode 100644
index 00000000..94f04380
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/model/Song.java
@@ -0,0 +1,415 @@
+package com.cappielloantonio.play.model;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.Ignore;
+import androidx.room.PrimaryKey;
+
+import org.jellyfin.apiclient.model.dto.BaseItemDto;
+import org.jellyfin.apiclient.model.dto.MediaSourceInfo;
+import org.jellyfin.apiclient.model.entities.ImageType;
+import org.jellyfin.apiclient.model.entities.MediaStream;
+
+import java.util.UUID;
+
+@Entity(tableName = "song")
+public class Song implements Parcelable {
+ @NonNull
+ @PrimaryKey
+ @ColumnInfo(name = "id")
+ private String id;
+
+ @ColumnInfo(name = "title")
+ private String title;
+
+ @ColumnInfo(name = "trackNumber")
+ private int trackNumber;
+
+ @ColumnInfo(name = "discNumber")
+ private int discNumber;
+
+ @ColumnInfo(name = "year")
+ private int year;
+
+ @ColumnInfo(name = "duration")
+ private long duration;
+
+ @ColumnInfo(name = "albumId")
+ private String albumId;
+
+ @ColumnInfo(name = "albumName")
+ private String albumName;
+
+ @ColumnInfo(name = "artistId")
+ private String artistId;
+
+ @ColumnInfo(name = "artistName")
+ private String artistName;
+
+ @ColumnInfo(name = "primary")
+ private String primary;
+
+ @ColumnInfo(name = "blurHash")
+ private String blurHash;
+
+ @ColumnInfo(name = "favorite")
+ private boolean favorite;
+
+ @ColumnInfo(name = "path")
+ private String path;
+
+ @ColumnInfo(name = "size")
+ private long size;
+
+ @ColumnInfo(name = "container")
+ private String container;
+
+ @ColumnInfo(name = "codec")
+ private String codec;
+
+ @ColumnInfo(name = "sampleRate")
+ private int sampleRate;
+
+ @ColumnInfo(name = "bitRate")
+ private int bitRate;
+
+ @ColumnInfo(name = "bitDepth")
+ private int bitDepth;
+
+ @ColumnInfo(name = "channels")
+ private int channels;
+
+ public Song(@NonNull String id, String title, int trackNumber, int discNumber, int year, long duration, String albumId, String albumName, String artistId, String artistName, String primary, String blurHash, boolean favorite, String path, long size, String container, String codec, int sampleRate, int bitRate, int bitDepth, int channels) {
+ this.id = id;
+ this.title = title;
+ this.trackNumber = trackNumber;
+ this.discNumber = discNumber;
+ this.year = year;
+ this.duration = duration;
+ this.albumId = albumId;
+ this.albumName = albumName;
+ this.artistId = artistId;
+ this.artistName = artistName;
+ this.primary = primary;
+ this.blurHash = blurHash;
+ this.favorite = favorite;
+ this.path = path;
+ this.size = size;
+ this.container = container;
+ this.codec = codec;
+ this.sampleRate = sampleRate;
+ this.bitRate = bitRate;
+ this.bitDepth = bitDepth;
+ this.channels = channels;
+ }
+
+ @Ignore
+ public Song(BaseItemDto itemDto) {
+ this.id = itemDto.getId();
+ this.title = itemDto.getName();
+ this.trackNumber = itemDto.getIndexNumber() != null ? itemDto.getIndexNumber() : 0;
+ this.discNumber = itemDto.getParentIndexNumber() != null ? itemDto.getParentIndexNumber() : 0;
+ this.year = itemDto.getProductionYear() != null ? itemDto.getProductionYear() : 0;
+ this.duration = itemDto.getRunTimeTicks() != null ? itemDto.getRunTimeTicks() / 10000 : 0;
+
+ this.albumId = itemDto.getAlbumId();
+ this.albumName = itemDto.getAlbum();
+
+ if (itemDto.getAlbumArtists().size() != 0) {
+ this.artistId = itemDto.getAlbumArtists().get(0).getId();
+ this.artistName = itemDto.getAlbumArtists().get(0).getName();
+ } else if (itemDto.getArtistItems().size() != 0) {
+ this.artistId = itemDto.getArtistItems().get(0).getId();
+ this.artistName = itemDto.getArtistItems().get(0).getName();
+ }
+
+ this.primary = itemDto.getAlbumPrimaryImageTag() != null ? albumId : null;
+ if (itemDto.getImageBlurHashes() != null && itemDto.getImageBlurHashes().get(ImageType.Primary) != null) {
+ this.blurHash = (String) itemDto.getImageBlurHashes().get(ImageType.Primary).values().toArray()[0];
+ }
+
+ this.favorite = itemDto.getUserData() != null && itemDto.getUserData().getIsFavorite();
+
+ if (itemDto.getMediaSources() != null && itemDto.getMediaSources().get(0) != null) {
+ MediaSourceInfo source = itemDto.getMediaSources().get(0);
+
+ this.path = source.getPath();
+ this.size = source.getSize() != null ? source.getSize() : 0;
+
+ this.container = source.getContainer();
+ this.bitRate = source.getBitrate() != null ? source.getBitrate() : 0;
+
+ if (source.getMediaStreams() != null && source.getMediaStreams().size() != 0) {
+ MediaStream stream = source.getMediaStreams().get(0);
+
+ this.codec = stream.getCodec();
+ this.sampleRate = stream.getSampleRate() != null ? stream.getSampleRate() : 0;
+ this.bitDepth = stream.getBitDepth() != null ? stream.getBitDepth() : 0;
+ this.channels = stream.getChannels() != null ? stream.getChannels() : 0;
+ }
+ }
+ }
+
+ @Ignore
+ public Song(String title, String albumName) {
+ this.id = UUID.randomUUID().toString();
+ this.title = title;
+ this.albumName = albumName;
+ }
+
+ @NonNull
+ public String getId() {
+ return id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public int getTrackNumber() {
+ return trackNumber;
+ }
+
+ public int getDiscNumber() {
+ return discNumber;
+ }
+
+ public int getYear() {
+ return year;
+ }
+
+ public long getDuration() {
+ return duration;
+ }
+
+ public String getAlbumId() {
+ return albumId;
+ }
+
+ public String getAlbumName() {
+ return albumName;
+ }
+
+ public String getArtistId() {
+ return artistId;
+ }
+
+ public String getArtistName() {
+ return artistName;
+ }
+
+ public String getPrimary() {
+ return primary;
+ }
+
+ public String getBlurHash() {
+ return blurHash;
+ }
+
+ public boolean isFavorite() {
+ return favorite;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public long getSize() {
+ return size;
+ }
+
+ public String getContainer() {
+ return container;
+ }
+
+ public String getCodec() {
+ return codec;
+ }
+
+ public int getSampleRate() {
+ return sampleRate;
+ }
+
+ public int getBitRate() {
+ return bitRate;
+ }
+
+ public int getBitDepth() {
+ return bitDepth;
+ }
+
+ public int getChannels() {
+ return channels;
+ }
+
+ public void setId(@NonNull String id) {
+ this.id = id;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public void setTrackNumber(int trackNumber) {
+ this.trackNumber = trackNumber;
+ }
+
+ public void setDiscNumber(int discNumber) {
+ this.discNumber = discNumber;
+ }
+
+ public void setYear(int year) {
+ this.year = year;
+ }
+
+ public void setDuration(long duration) {
+ this.duration = duration;
+ }
+
+ public void setAlbumId(String albumId) {
+ this.albumId = albumId;
+ }
+
+ public void setAlbumName(String albumName) {
+ this.albumName = albumName;
+ }
+
+ public void setArtistId(String artistId) {
+ this.artistId = artistId;
+ }
+
+ public void setArtistName(String artistName) {
+ this.artistName = artistName;
+ }
+
+ public void setPrimary(String primary) {
+ this.primary = primary;
+ }
+
+ public void setBlurHash(String blurHash) {
+ this.blurHash = blurHash;
+ }
+
+ public void setFavorite(boolean favorite) {
+ this.favorite = favorite;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public void setSize(long size) {
+ this.size = size;
+ }
+
+ public void setContainer(String container) {
+ this.container = container;
+ }
+
+ public void setCodec(String codec) {
+ this.codec = codec;
+ }
+
+ public void setSampleRate(int sampleRate) {
+ this.sampleRate = sampleRate;
+ }
+
+ public void setBitRate(int bitRate) {
+ this.bitRate = bitRate;
+ }
+
+ public void setBitDepth(int bitDepth) {
+ this.bitDepth = bitDepth;
+ }
+
+ public void setChannels(int channels) {
+ this.channels = channels;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Song song = (Song) o;
+ return id.equals(song.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return id;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(this.id);
+ dest.writeString(this.title);
+ dest.writeInt(this.trackNumber);
+ dest.writeInt(this.discNumber);
+ dest.writeInt(this.year);
+ dest.writeLong(this.duration);
+ dest.writeString(this.albumId);
+ dest.writeString(this.albumName);
+ dest.writeString(this.artistId);
+ dest.writeString(this.artistName);
+ dest.writeString(this.primary);
+ dest.writeString(Boolean.toString(favorite));
+ dest.writeString(this.blurHash);
+ dest.writeString(this.path);
+ dest.writeLong(this.size);
+ dest.writeString(this.container);
+ dest.writeString(this.codec);
+ dest.writeInt(this.sampleRate);
+ dest.writeInt(this.bitRate);
+ dest.writeInt(this.bitDepth);
+ dest.writeInt(this.channels);
+ }
+
+ protected Song(Parcel in) {
+ this.id = in.readString();
+ this.title = in.readString();
+ this.trackNumber = in.readInt();
+ this.discNumber = in.readInt();
+ this.year = in.readInt();
+ this.duration = in.readLong();
+ this.albumId = in.readString();
+ this.albumName = in.readString();
+ this.artistId = in.readString();
+ this.artistName = in.readString();
+ this.primary = in.readString();
+ this.favorite = Boolean.parseBoolean(in.readString());
+ this.blurHash = in.readString();
+ this.path = in.readString();
+ this.size = in.readLong();
+ this.container = in.readString();
+ this.codec = in.readString();
+ this.sampleRate = in.readInt();
+ this.bitRate = in.readInt();
+ this.bitDepth = in.readInt();
+ this.channels = in.readInt();
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ public Song createFromParcel(Parcel source) {
+ return new Song(source);
+ }
+
+ public Song[] newArray(int size) {
+ return new Song[size];
+ }
+ };
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/repository/AlbumRepository.java b/app/src/main/java/com/cappielloantonio/play/repository/AlbumRepository.java
new file mode 100644
index 00000000..906dc45d
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/repository/AlbumRepository.java
@@ -0,0 +1,127 @@
+package com.cappielloantonio.play.repository;
+
+import android.app.Application;
+
+import androidx.lifecycle.LiveData;
+
+import com.cappielloantonio.play.database.AppDatabase;
+import com.cappielloantonio.play.database.dao.AlbumDao;
+import com.cappielloantonio.play.model.Album;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AlbumRepository {
+ private AlbumDao albumDao;
+ private LiveData> listLiveAlbums;
+
+ public AlbumRepository(Application application) {
+ AppDatabase database = AppDatabase.getInstance(application);
+ albumDao = database.albumDao();
+ listLiveAlbums = albumDao.getAll();
+ }
+
+ public LiveData> getListLiveAlbums() {
+ return listLiveAlbums;
+ }
+
+ public boolean exist(Album album) {
+ boolean exist = false;
+
+ ExistThreadSafe existThread = new ExistThreadSafe(albumDao, album);
+ Thread thread = new Thread(existThread);
+ thread.start();
+
+ try {
+ thread.join();
+ exist = existThread.exist();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ return exist;
+ }
+
+ public void insert(Album album) {
+ InsertThreadSafe insert = new InsertThreadSafe(albumDao, album);
+ Thread thread = new Thread(insert);
+ thread.start();
+ }
+
+ public void insertAll(ArrayList albums) {
+ InsertAllThreadSafe insertAll = new InsertAllThreadSafe(albumDao, albums);
+ Thread thread = new Thread(insertAll);
+ thread.start();
+ }
+
+ public void delete(Album album) {
+ DeleteThreadSafe delete = new DeleteThreadSafe(albumDao, album);
+ Thread thread = new Thread(delete);
+ thread.start();
+ }
+
+ private static class ExistThreadSafe implements Runnable {
+ private AlbumDao albumDao;
+ private Album album;
+ private boolean exist = false;
+
+ public ExistThreadSafe(AlbumDao albumDao, Album album) {
+ this.albumDao = albumDao;
+ this.album = album;
+ }
+
+ @Override
+ public void run() {
+ exist = albumDao.exist(album.getId());
+ }
+
+ public boolean exist() {
+ return exist;
+ }
+ }
+
+ private static class InsertThreadSafe implements Runnable {
+ private AlbumDao albumDao;
+ private Album album;
+
+ public InsertThreadSafe(AlbumDao albumDao, Album album) {
+ this.albumDao = albumDao;
+ this.album = album;
+ }
+
+ @Override
+ public void run() {
+ albumDao.insert(album);
+ }
+ }
+
+ private static class InsertAllThreadSafe implements Runnable {
+ private AlbumDao albumDao;
+ private ArrayList albums;
+
+ public InsertAllThreadSafe(AlbumDao albumDao, ArrayList albums) {
+ this.albumDao = albumDao;
+ this.albums = albums;
+ }
+
+ @Override
+ public void run() {
+ albumDao.insertAll(albums);
+ }
+ }
+
+ private static class DeleteThreadSafe implements Runnable {
+ private AlbumDao albumDao;
+ private Album album;
+
+ public DeleteThreadSafe(AlbumDao albumDao, Album album) {
+ this.albumDao = albumDao;
+ this.album = album;
+ }
+
+ @Override
+ public void run() {
+ albumDao.delete(album);
+ }
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/repository/ArtistRepository.java b/app/src/main/java/com/cappielloantonio/play/repository/ArtistRepository.java
new file mode 100644
index 00000000..cb531828
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/repository/ArtistRepository.java
@@ -0,0 +1,128 @@
+package com.cappielloantonio.play.repository;
+
+import android.app.Application;
+
+import androidx.lifecycle.LiveData;
+
+import com.cappielloantonio.play.database.AppDatabase;
+import com.cappielloantonio.play.database.dao.ArtistDao;
+import com.cappielloantonio.play.model.Artist;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ArtistRepository {
+ private ArtistDao artistDao;
+ private LiveData> listLiveArtists;
+
+ public ArtistRepository(Application application) {
+ AppDatabase database = AppDatabase.getInstance(application);
+ artistDao = database.artistDao();
+ listLiveArtists = artistDao.getAll();
+ }
+
+ public LiveData> getListLiveArtists() {
+ return listLiveArtists;
+ }
+
+ public boolean exist(Artist artist) {
+ boolean exist = false;
+
+ ExistThreadSafe existThread = new ExistThreadSafe(artistDao, artist);
+ Thread thread = new Thread(existThread);
+ thread.start();
+
+ try {
+ thread.join();
+ exist = existThread.exist();
+ }
+ catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ return exist;
+ }
+
+ public void insert(Artist artist) {
+ InsertThreadSafe insert = new InsertThreadSafe(artistDao, artist);
+ Thread thread = new Thread(insert);
+ thread.start();
+ }
+
+ public void insertAll(ArrayList artists) {
+ InsertAllThreadSafe insertAll = new InsertAllThreadSafe(artistDao, artists);
+ Thread thread = new Thread(insertAll);
+ thread.start();
+ }
+
+ public void delete(Artist artist) {
+ DeleteThreadSafe delete = new DeleteThreadSafe(artistDao, artist);
+ Thread thread = new Thread(delete);
+ thread.start();
+ }
+
+ private static class ExistThreadSafe implements Runnable {
+ private ArtistDao artistDao;
+ private Artist artist;
+ private boolean exist = false;
+
+ public ExistThreadSafe(ArtistDao artistDao, Artist artist) {
+ this.artistDao = artistDao;
+ this.artist = artist;
+ }
+
+ @Override
+ public void run() {
+ exist = artistDao.exist(artist.getId());
+ }
+
+ public boolean exist() {
+ return exist;
+ }
+ }
+
+ private static class InsertThreadSafe implements Runnable {
+ private ArtistDao artistDao;
+ private Artist artist;
+
+ public InsertThreadSafe(ArtistDao artistDao, Artist artist) {
+ this.artistDao = artistDao;
+ this.artist = artist;
+ }
+
+ @Override
+ public void run() {
+ artistDao.insert(artist);
+ }
+ }
+
+ private static class InsertAllThreadSafe implements Runnable {
+ private ArtistDao artistDao;
+ private ArrayList artists;
+
+ public InsertAllThreadSafe(ArtistDao artistDao, ArrayList artists) {
+ this.artistDao = artistDao;
+ this.artists = artists;
+ }
+
+ @Override
+ public void run() {
+ artistDao.insertAll(artists);
+ }
+ }
+
+ private static class DeleteThreadSafe implements Runnable {
+ private ArtistDao artistDao;
+ private Artist artist;
+
+ public DeleteThreadSafe(ArtistDao artistDao, Artist artist) {
+ this.artistDao = artistDao;
+ this.artist = artist;
+ }
+
+ @Override
+ public void run() {
+ artistDao.delete(artist);
+ }
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/repository/GenreRepository.java b/app/src/main/java/com/cappielloantonio/play/repository/GenreRepository.java
new file mode 100644
index 00000000..f3d8a1f1
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/repository/GenreRepository.java
@@ -0,0 +1,128 @@
+package com.cappielloantonio.play.repository;
+
+import android.app.Application;
+
+import androidx.lifecycle.LiveData;
+
+import com.cappielloantonio.play.database.AppDatabase;
+import com.cappielloantonio.play.database.dao.GenreDao;
+import com.cappielloantonio.play.model.Genre;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class GenreRepository {
+ private GenreDao genreDao;
+ private LiveData> listLiveGenres;
+
+ public GenreRepository(Application application) {
+ AppDatabase database = AppDatabase.getInstance(application);
+ genreDao = database.genreDao();
+ listLiveGenres = genreDao.getAll();
+ }
+
+ public LiveData> getListLiveGenres() {
+ return listLiveGenres;
+ }
+
+ public boolean exist(Genre genre) {
+ boolean exist = false;
+
+ ExistThreadSafe existThread = new ExistThreadSafe(genreDao, genre);
+ Thread thread = new Thread(existThread);
+ thread.start();
+
+ try {
+ thread.join();
+ exist = existThread.exist();
+ }
+ catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ return exist;
+ }
+
+ public void insert(Genre genre) {
+ InsertThreadSafe insert = new InsertThreadSafe(genreDao, genre);
+ Thread thread = new Thread(insert);
+ thread.start();
+ }
+
+ public void insertAll(ArrayList genres) {
+ InsertAllThreadSafe insertAll = new InsertAllThreadSafe(genreDao, genres);
+ Thread thread = new Thread(insertAll);
+ thread.start();
+ }
+
+ public void delete(Genre genre) {
+ DeleteThreadSafe delete = new DeleteThreadSafe(genreDao, genre);
+ Thread thread = new Thread(delete);
+ thread.start();
+ }
+
+ private static class ExistThreadSafe implements Runnable {
+ private GenreDao genreDao;
+ private Genre genre;
+ private boolean exist = false;
+
+ public ExistThreadSafe(GenreDao genreDao, Genre genre) {
+ this.genreDao = genreDao;
+ this.genre = genre;
+ }
+
+ @Override
+ public void run() {
+ exist = genreDao.exist(genre.getId());
+ }
+
+ public boolean exist() {
+ return exist;
+ }
+ }
+
+ private static class InsertThreadSafe implements Runnable {
+ private GenreDao genreDao;
+ private Genre genre;
+
+ public InsertThreadSafe(GenreDao genreDao, Genre genre) {
+ this.genreDao = genreDao;
+ this.genre = genre;
+ }
+
+ @Override
+ public void run() {
+ genreDao.insert(genre);
+ }
+ }
+
+ private static class InsertAllThreadSafe implements Runnable {
+ private GenreDao genreDao;
+ private ArrayList genres;
+
+ public InsertAllThreadSafe(GenreDao genreDao, ArrayList genres) {
+ this.genreDao = genreDao;
+ this.genres = genres;
+ }
+
+ @Override
+ public void run() {
+ genreDao.insertAll(genres);
+ }
+ }
+
+ private static class DeleteThreadSafe implements Runnable {
+ private GenreDao genreDao;
+ private Genre genre;
+
+ public DeleteThreadSafe(GenreDao genreDao, Genre genre) {
+ this.genreDao = genreDao;
+ this.genre = genre;
+ }
+
+ @Override
+ public void run() {
+ genreDao.delete(genre);
+ }
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/repository/PlaylistRepository.java b/app/src/main/java/com/cappielloantonio/play/repository/PlaylistRepository.java
new file mode 100644
index 00000000..b76e9ca9
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/repository/PlaylistRepository.java
@@ -0,0 +1,128 @@
+package com.cappielloantonio.play.repository;
+
+import android.app.Application;
+
+import androidx.lifecycle.LiveData;
+
+import com.cappielloantonio.play.database.AppDatabase;
+import com.cappielloantonio.play.database.dao.PlaylistDao;
+import com.cappielloantonio.play.model.Playlist;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PlaylistRepository {
+ private PlaylistDao playlistDao;
+ private LiveData> listLivePlaylists;
+
+ public PlaylistRepository(Application application) {
+ AppDatabase database = AppDatabase.getInstance(application);
+ playlistDao = database.playlistDao();
+ listLivePlaylists = playlistDao.getAll();
+ }
+
+ public LiveData> getListLivePlaylists() {
+ return listLivePlaylists;
+ }
+
+ public boolean exist(Playlist playlist) {
+ boolean exist = false;
+
+ ExistThreadSafe existThread = new ExistThreadSafe(playlistDao, playlist);
+ Thread thread = new Thread(existThread);
+ thread.start();
+
+ try {
+ thread.join();
+ exist = existThread.exist();
+ }
+ catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ return exist;
+ }
+
+ public void insert(Playlist playlist) {
+ InsertThreadSafe insert = new InsertThreadSafe(playlistDao, playlist);
+ Thread thread = new Thread(insert);
+ thread.start();
+ }
+
+ public void insertAll(ArrayList playlists) {
+ InsertAllThreadSafe insertAll = new InsertAllThreadSafe(playlistDao, playlists);
+ Thread thread = new Thread(insertAll);
+ thread.start();
+ }
+
+ public void delete(Playlist playlist) {
+ DeleteThreadSafe delete = new DeleteThreadSafe(playlistDao, playlist);
+ Thread thread = new Thread(delete);
+ thread.start();
+ }
+
+ private static class ExistThreadSafe implements Runnable {
+ private PlaylistDao playlistDao;
+ private Playlist playlist;
+ private boolean exist = false;
+
+ public ExistThreadSafe(PlaylistDao playlistDao, Playlist playlist) {
+ this.playlistDao = playlistDao;
+ this.playlist = playlist;
+ }
+
+ @Override
+ public void run() {
+ exist = playlistDao.exist(playlist.getId());
+ }
+
+ public boolean exist() {
+ return exist;
+ }
+ }
+
+ private static class InsertThreadSafe implements Runnable {
+ private PlaylistDao playlistDao;
+ private Playlist playlist;
+
+ public InsertThreadSafe(PlaylistDao playlistDao, Playlist playlist) {
+ this.playlistDao = playlistDao;
+ this.playlist = playlist;
+ }
+
+ @Override
+ public void run() {
+ playlistDao.insert(playlist);
+ }
+ }
+
+ private static class InsertAllThreadSafe implements Runnable {
+ private PlaylistDao playlistDao;
+ private ArrayList playlists;
+
+ public InsertAllThreadSafe(PlaylistDao playlistDao, ArrayList playlists) {
+ this.playlistDao = playlistDao;
+ this.playlists = playlists;
+ }
+
+ @Override
+ public void run() {
+ playlistDao.insertAll(playlists);
+ }
+ }
+
+ private static class DeleteThreadSafe implements Runnable {
+ private PlaylistDao playlistDao;
+ private Playlist playlist;
+
+ public DeleteThreadSafe(PlaylistDao playlistDao, Playlist playlist) {
+ this.playlistDao = playlistDao;
+ this.playlist = playlist;
+ }
+
+ @Override
+ public void run() {
+ playlistDao.delete(playlist);
+ }
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java b/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java
new file mode 100644
index 00000000..bdcf8272
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java
@@ -0,0 +1,128 @@
+package com.cappielloantonio.play.repository;
+
+import android.app.Application;
+
+import androidx.lifecycle.LiveData;
+
+import com.cappielloantonio.play.database.AppDatabase;
+import com.cappielloantonio.play.database.dao.SongDao;
+import com.cappielloantonio.play.model.Song;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SongRepository {
+ private SongDao songDao;
+ private LiveData> listLiveSongs;
+
+ public SongRepository(Application application) {
+ AppDatabase database = AppDatabase.getInstance(application);
+ songDao = database.songDao();
+ listLiveSongs = songDao.getAll();
+ }
+
+ public LiveData> getListLiveSongs() {
+ return listLiveSongs;
+ }
+
+ public boolean exist(Song song) {
+ boolean exist = false;
+
+ ExistThreadSafe existThread = new ExistThreadSafe(songDao, song);
+ Thread thread = new Thread(existThread);
+ thread.start();
+
+ try {
+ thread.join();
+ exist = existThread.exist();
+ }
+ catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ return exist;
+ }
+
+ public void insert(Song song) {
+ InsertThreadSafe insert = new InsertThreadSafe(songDao, song);
+ Thread thread = new Thread(insert);
+ thread.start();
+ }
+
+ public void insertAll(ArrayList songs) {
+ InsertAllThreadSafe insertAll = new InsertAllThreadSafe(songDao, songs);
+ Thread thread = new Thread(insertAll);
+ thread.start();
+ }
+
+ public void delete(Song song) {
+ DeleteThreadSafe delete = new DeleteThreadSafe(songDao, song);
+ Thread thread = new Thread(delete);
+ thread.start();
+ }
+
+ private static class ExistThreadSafe implements Runnable {
+ private SongDao songDao;
+ private Song song;
+ private boolean exist = false;
+
+ public ExistThreadSafe(SongDao songDao, Song song) {
+ this.songDao = songDao;
+ this.song = song;
+ }
+
+ @Override
+ public void run() {
+ exist = songDao.exist(song.getId());
+ }
+
+ public boolean exist() {
+ return exist;
+ }
+ }
+
+ private static class InsertThreadSafe implements Runnable {
+ private SongDao songDao;
+ private Song song;
+
+ public InsertThreadSafe(SongDao songDao, Song song) {
+ this.songDao = songDao;
+ this.song = song;
+ }
+
+ @Override
+ public void run() {
+ songDao.insert(song);
+ }
+ }
+
+ private static class InsertAllThreadSafe implements Runnable {
+ private SongDao songDao;
+ private ArrayList songs;
+
+ public InsertAllThreadSafe(SongDao songDao, ArrayList songs) {
+ this.songDao = songDao;
+ this.songs = songs;
+ }
+
+ @Override
+ public void run() {
+ songDao.insertAll(songs);
+ }
+ }
+
+ private static class DeleteThreadSafe implements Runnable {
+ private SongDao songDao;
+ private Song song;
+
+ public DeleteThreadSafe(SongDao songDao, Song song) {
+ this.songDao = songDao;
+ this.song = song;
+ }
+
+ @Override
+ public void run() {
+ songDao.delete(song);
+ }
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/ui/activities/MainActivity.java b/app/src/main/java/com/cappielloantonio/play/ui/activities/MainActivity.java
new file mode 100644
index 00000000..39042ab1
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/ui/activities/MainActivity.java
@@ -0,0 +1,117 @@
+package com.cappielloantonio.play.ui.activities;
+
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.fragment.app.FragmentManager;
+import androidx.navigation.NavController;
+import androidx.navigation.fragment.NavHostFragment;
+import androidx.navigation.ui.NavigationUI;
+
+import com.cappielloantonio.play.App;
+import com.cappielloantonio.play.R;
+import com.cappielloantonio.play.databinding.ActivityMainBinding;
+import com.cappielloantonio.play.ui.activities.base.BaseActivity;
+import com.cappielloantonio.play.util.PreferenceUtil;
+import com.google.android.material.bottomnavigation.BottomNavigationView;
+
+import org.jellyfin.apiclient.interaction.EmptyResponse;
+import org.jellyfin.apiclient.interaction.Response;
+import org.jellyfin.apiclient.model.session.ClientCapabilities;
+import org.jellyfin.apiclient.model.system.SystemInfo;
+
+import java.util.Objects;
+
+public class MainActivity extends BaseActivity {
+ private static final String TAG = "MainActivity";
+
+ private ActivityMainBinding activityMainBinding;
+
+ private FragmentManager fragmentManager;
+ private NavHostFragment navHostFragment;
+ private BottomNavigationView bottomNavigationView;
+ public NavController navController;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ activityMainBinding = ActivityMainBinding.inflate(getLayoutInflater());
+ View view = activityMainBinding.getRoot();
+ setContentView(view);
+
+ init();
+ }
+
+ public void init() {
+ fragmentManager = getSupportFragmentManager();
+ bottomNavigationView = findViewById(R.id.bottom_navigation);
+
+ navHostFragment = (NavHostFragment) fragmentManager.findFragmentById(R.id.nav_host_fragment);
+ navController = navHostFragment.getNavController();
+ NavigationUI.setupWithNavController(bottomNavigationView, navController);
+
+ if (PreferenceUtil.getInstance(this).getToken() != null) {
+ checkPreviousSession();
+ } else {
+ goToLogin();
+ }
+ }
+
+ private void checkPreviousSession() {
+ App.getApiClientInstance(getApplicationContext()).ChangeServerLocation(PreferenceUtil.getInstance(this).getServer());
+ App.getApiClientInstance(getApplicationContext()).SetAuthenticationInfo(PreferenceUtil.getInstance(this).getToken(), PreferenceUtil.getInstance(this).getUser());
+ App.getApiClientInstance(getApplicationContext()).GetSystemInfoAsync(new Response() {
+ @Override
+ public void onResponse(SystemInfo result) {
+ ClientCapabilities clientCapabilities = new ClientCapabilities();
+ clientCapabilities.setSupportsMediaControl(true);
+ clientCapabilities.setSupportsPersistentIdentifier(true);
+
+ App.getApiClientInstance(getApplicationContext()).ensureWebSocket();
+ App.getApiClientInstance(getApplicationContext()).ReportCapabilities(clientCapabilities, new EmptyResponse());
+
+ goFromLogin();
+ }
+
+ @Override
+ public void onError(Exception exception) {
+ goToLogin();
+ }
+ });
+ }
+
+ public void goToLogin() {
+ if (Objects.requireNonNull(navController.getCurrentDestination()).getId() == R.id.landingFragment)
+ navController.navigate(R.id.action_landingFragment_to_loginFragment);
+ }
+
+ public void goToSync() {
+ if (Objects.requireNonNull(navController.getCurrentDestination()).getId() == R.id.landingFragment) {
+ navController.navigate(R.id.action_landingFragment_to_syncFragment);
+ } else if (Objects.requireNonNull(navController.getCurrentDestination()).getId() == R.id.loginFragment) {
+ navController.navigate(R.id.action_loginFragment_to_syncFragment);
+ } else if (Objects.requireNonNull(navController.getCurrentDestination()).getId() == R.id.homeFragment) {
+ navController.navigate(R.id.action_homeFragment_to_syncFragment);
+ }
+ }
+
+ public void goToHome() {
+ bottomNavigationView.setVisibility(View.VISIBLE);
+
+ if (Objects.requireNonNull(navController.getCurrentDestination()).getId() == R.id.landingFragment) {
+ navController.navigate(R.id.action_landingFragment_to_homeFragment);
+ } else if (Objects.requireNonNull(navController.getCurrentDestination()).getId() == R.id.syncFragment) {
+ navController.navigate(R.id.action_syncFragment_to_homeFragment);
+ } else if (Objects.requireNonNull(navController.getCurrentDestination()).getId() == R.id.loginFragment) {
+ navController.navigate(R.id.action_loginFragment_to_homeFragment);
+ }
+ }
+
+ public void goFromLogin() {
+ if (PreferenceUtil.getInstance(getApplicationContext()).getSync()) {
+ goToHome();
+ } else {
+ goToSync();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/play/ui/activities/base/BaseActivity.java b/app/src/main/java/com/cappielloantonio/play/ui/activities/base/BaseActivity.java
new file mode 100644
index 00000000..b4868a95
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/ui/activities/base/BaseActivity.java
@@ -0,0 +1,83 @@
+package com.cappielloantonio.play.ui.activities.base;
+
+import android.Manifest;
+import android.content.Intent;
+import android.os.PowerManager;
+import android.provider.Settings;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.cappielloantonio.play.R;
+
+import java.util.List;
+
+import pub.devrel.easypermissions.AppSettingsDialog;
+import pub.devrel.easypermissions.EasyPermissions;
+
+public class BaseActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks {
+ public static final int REQUEST_PERM_ACCESS = 1;
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ checkPermissions();
+// checkBatteryOptimization();
+ }
+
+ private void checkBatteryOptimization() {
+ if (detectBatteryOptimization()) {
+ showBatteryOptimizationDialog();
+ }
+ }
+
+ private boolean detectBatteryOptimization() {
+ String packageName = getPackageName();
+ PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
+ return !powerManager.isIgnoringBatteryOptimizations(packageName);
+ }
+
+ private void showBatteryOptimizationDialog() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.battery_optimizations_message)
+ .setTitle(R.string.battery_optimizations_title)
+ .setNegativeButton(R.string.ignore, null)
+ .setPositiveButton(R.string.disable, (dialog, id) -> openPowerSettings())
+ .show();
+ }
+
+ private void openPowerSettings() {
+ Intent intent = new Intent();
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
+ startActivity(intent);
+ }
+
+ private void checkPermissions() {
+ String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE};
+
+ if (!EasyPermissions.hasPermissions(this, permissions)) {
+ EasyPermissions.requestPermissions(this, getString(R.string.storage_permission_rationale), REQUEST_PERM_ACCESS, permissions);
+ }
+ }
+
+ @Override
+ public void onPermissionsGranted(int requestCode, @NonNull List perms) {
+
+ }
+
+ @Override
+ public void onPermissionsDenied(int requestCode, @NonNull List perms) {
+ if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
+ new AppSettingsDialog.Builder(this).build().show();
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+ EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/HomeFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/HomeFragment.java
new file mode 100644
index 00000000..4e3e69e3
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/HomeFragment.java
@@ -0,0 +1,91 @@
+package com.cappielloantonio.play.ui.fragment;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.LinearLayoutManager;
+
+import com.cappielloantonio.play.adapter.DiscoverSongAdapter;
+import com.cappielloantonio.play.adapter.RecentMusicAdapter;
+import com.cappielloantonio.play.databinding.FragmentHomeBinding;
+import com.cappielloantonio.play.ui.activities.MainActivity;
+import com.cappielloantonio.play.util.PreferenceUtil;
+import com.cappielloantonio.play.viewmodel.HomeViewModel;
+
+public class HomeFragment extends Fragment implements RecentMusicAdapter.ItemClickListener {
+ private static final String TAG = "CategoriesFragment";
+
+ private FragmentHomeBinding bind;
+ private MainActivity activity;
+ private HomeViewModel homeViewModel;
+
+ private DiscoverSongAdapter discoverSongAdapter;
+ private RecentMusicAdapter recentMusicAdapter;
+ private RecentMusicAdapter mostPlayedMusicAdapter;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ activity = (MainActivity) getActivity();
+
+ bind = FragmentHomeBinding.inflate(inflater, container, false);
+ View view = bind.getRoot();
+ homeViewModel = new ViewModelProvider(requireActivity()).get(HomeViewModel.class);
+
+ init();
+ initDiscoverSongSlideView();
+ initRecentPlayedSongView();
+ initMostPlayedSongView();
+
+ return view;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ bind = null;
+ }
+
+ private void init() {
+ bind.resyncButton.setOnClickListener(v -> {
+ PreferenceUtil.getInstance(requireContext()).setSync(false);
+ activity.goToSync();
+ });
+ }
+
+ private void initDiscoverSongSlideView() {
+ discoverSongAdapter = new DiscoverSongAdapter(requireContext(), homeViewModel.getDiscoverSongList());
+ bind.discoverSongViewPager.setAdapter(discoverSongAdapter);
+ bind.discoverSongViewPager.setPageMargin(20);
+ }
+
+ private void initRecentPlayedSongView() {
+ bind.recentlyPlayedTracksRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false));
+ bind.recentlyPlayedTracksRecyclerView.setHasFixedSize(true);
+
+ recentMusicAdapter = new RecentMusicAdapter(requireContext(), homeViewModel.getRecentSongList());
+ recentMusicAdapter.setClickListener(this);
+ bind.recentlyPlayedTracksRecyclerView.setAdapter(recentMusicAdapter);
+ }
+
+ private void initMostPlayedSongView() {
+ bind.mostPlayedTracksRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false));
+ bind.mostPlayedTracksRecyclerView.setHasFixedSize(true);
+
+ mostPlayedMusicAdapter = new RecentMusicAdapter(requireContext(), homeViewModel.getMostPlayedSongList());
+ mostPlayedMusicAdapter.setClickListener(this);
+ bind.mostPlayedTracksRecyclerView.setAdapter(mostPlayedMusicAdapter);
+ }
+
+ @Override
+ public void onItemClick(View view, int position) {
+ Toast.makeText(requireContext(), "Click: " + position, Toast.LENGTH_SHORT).show();
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/LandingFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/LandingFragment.java
new file mode 100644
index 00000000..f517290e
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/LandingFragment.java
@@ -0,0 +1,17 @@
+package com.cappielloantonio.play.ui.fragment;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.fragment.app.Fragment;
+
+import com.cappielloantonio.play.R;
+
+public class LandingFragment extends Fragment {
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_landing, container, false);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/LibraryFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/LibraryFragment.java
new file mode 100644
index 00000000..020710ab
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/LibraryFragment.java
@@ -0,0 +1,101 @@
+package com.cappielloantonio.play.ui.fragment;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+
+import com.cappielloantonio.play.adapter.AlbumAdapter;
+import com.cappielloantonio.play.adapter.ArtistAdapter;
+import com.cappielloantonio.play.adapter.GenreAdapter;
+import com.cappielloantonio.play.adapter.PlaylistAdapter;
+import com.cappielloantonio.play.databinding.FragmentLibraryBinding;
+import com.cappielloantonio.play.model.Genre;
+import com.cappielloantonio.play.model.Playlist;
+import com.cappielloantonio.play.ui.activities.MainActivity;
+import com.cappielloantonio.play.viewmodel.LibraryViewModel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class LibraryFragment extends Fragment {
+ private static final String TAG = "LibraryFragment";
+
+ private FragmentLibraryBinding bind;
+ private MainActivity activity;
+ private LibraryViewModel libraryViewModel;
+
+ private AlbumAdapter albumAdapter;
+ private ArtistAdapter artistAdapter;
+ private GenreAdapter genreAdapter;
+ private PlaylistAdapter playlistAdapter;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ activity = (MainActivity) getActivity();
+
+ bind = FragmentLibraryBinding.inflate(inflater, container, false);
+ View view = bind.getRoot();
+ libraryViewModel = new ViewModelProvider(requireActivity()).get(LibraryViewModel.class);
+
+ initAlbumView();
+ initArtistView();
+ initGenreView();
+ initPlaylistView();
+
+ return view;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ bind = null;
+ }
+
+ private void initAlbumView() {
+ bind.albumRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false));
+ bind.albumRecyclerView.setHasFixedSize(true);
+
+ albumAdapter = new AlbumAdapter(requireContext(), libraryViewModel.getAlbumSample());
+ albumAdapter.setClickListener((view, position) -> Toast.makeText(requireContext(), "Album: " + position, Toast.LENGTH_SHORT).show());
+ bind.albumRecyclerView.setAdapter(albumAdapter);
+ }
+
+ private void initArtistView() {
+ bind.artistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false));
+ bind.artistRecyclerView.setHasFixedSize(true);
+
+ artistAdapter = new ArtistAdapter(requireContext(), libraryViewModel.getArtistSample());
+ artistAdapter.setClickListener((view, position) -> Toast.makeText(requireContext(), "Artist: " + position, Toast.LENGTH_SHORT).show());
+ bind.artistRecyclerView.setAdapter(artistAdapter);
+ }
+
+ private void initGenreView() {
+ bind.genreRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 3, GridLayoutManager.HORIZONTAL, false));
+ bind.genreRecyclerView.setHasFixedSize(true);
+
+ genreAdapter = new GenreAdapter(requireContext(), new ArrayList<>());
+ genreAdapter.setClickListener((view, position) -> Toast.makeText(requireContext(), "Genre: " + position, Toast.LENGTH_SHORT).show());
+ bind.genreRecyclerView.setAdapter(genreAdapter);
+ libraryViewModel.getGenreList().observe(requireActivity(), genres -> genreAdapter.setItems(genres));
+ }
+
+ private void initPlaylistView() {
+ bind.playlistRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
+ bind.playlistRecyclerView.setHasFixedSize(true);
+
+ playlistAdapter = new PlaylistAdapter(requireContext(), libraryViewModel.getPlaylist());
+ playlistAdapter.setClickListener((view, position) -> Toast.makeText(requireContext(), "Playlist: " + position, Toast.LENGTH_SHORT).show());
+ bind.playlistRecyclerView.setAdapter(playlistAdapter);
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/LoginFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/LoginFragment.java
new file mode 100644
index 00000000..5fea757e
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/LoginFragment.java
@@ -0,0 +1,123 @@
+package com.cappielloantonio.play.ui.fragment;
+
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.cappielloantonio.play.App;
+import com.cappielloantonio.play.databinding.FragmentLoginBinding;
+import com.cappielloantonio.play.ui.activities.MainActivity;
+import com.cappielloantonio.play.util.PreferenceUtil;
+
+import org.jellyfin.apiclient.interaction.Response;
+import org.jellyfin.apiclient.model.system.SystemInfo;
+import org.jellyfin.apiclient.model.users.AuthenticationResult;
+
+public class LoginFragment extends Fragment {
+ private static final String TAG = "LoginFragment";
+
+ private FragmentLoginBinding bind;
+ private MainActivity activity;
+
+ // private TextView usernameTextView;
+// private TextView passwordTextView;
+// private TextView serverTextView;
+ private String username;
+ private String password;
+ private String server;
+
+// private Button loginButton;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ activity = (MainActivity) getActivity();
+
+ bind = FragmentLoginBinding.inflate(inflater, container, false);
+ View view = bind.getRoot();
+ init();
+
+ return view;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ bind = null;
+ }
+
+ private void init() {
+ bind.loginButton.setOnClickListener(v -> {
+ if (validateInput()) {
+ saveServerPreference(username, server);
+ authenticate();
+ }
+ });
+ }
+
+ private boolean validateInput() {
+ username = bind.usernameTextView.getText().toString().trim();
+ password = bind.passwordTextView.getText().toString().trim();
+ server = bind.serverTextView.getText().toString().trim();
+
+ if (TextUtils.isEmpty(username)) {
+ Toast.makeText(requireContext(), "Empty username", Toast.LENGTH_SHORT).show();
+ return false;
+ }
+
+ if (TextUtils.isEmpty(server)) {
+ Toast.makeText(requireContext(), "Empty server", Toast.LENGTH_SHORT).show();
+ return false;
+ }
+
+ return true;
+ }
+
+ private void saveServerPreference(String user, String server) {
+ PreferenceUtil.getInstance(requireContext()).setUser(user);
+ PreferenceUtil.getInstance(requireContext()).setServer(server);
+ }
+
+ private void authenticate() {
+ App.getApiClientInstance(requireContext()).ChangeServerLocation(server);
+ App.getApiClientInstance(requireContext()).AuthenticateUserAsync(username, password, new Response() {
+ @Override
+ public void onResponse(AuthenticationResult result) {
+ if (result.getAccessToken() == null) return;
+ enter(result.getUser().getId(), result.getAccessToken());
+ }
+
+ @Override
+ public void onError(Exception exception) {
+ if (exception.getMessage().contains("AuthFailureError")) {
+ Toast.makeText(requireContext(), "Fail to authenticate", Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(requireContext(), "Server unreachable", Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ }
+
+ private void enter(String user, String token) {
+ App.getApiClientInstance(requireContext()).GetSystemInfoAsync(new Response() {
+ @Override
+ public void onResponse(SystemInfo result) {
+ if (result.getVersion().charAt(0) == '1') {
+ PreferenceUtil.getInstance(requireContext()).setUser(user);
+ PreferenceUtil.getInstance(requireContext()).setToken(token);
+
+ activity.goFromLogin();
+ } else {
+ Toast.makeText(requireContext(), "Error version", Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/SearchFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/SearchFragment.java
new file mode 100644
index 00000000..c608a3f5
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/SearchFragment.java
@@ -0,0 +1,118 @@
+package com.cappielloantonio.play.ui.fragment;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.cappielloantonio.play.databinding.FragmentSearchBinding;
+import com.cappielloantonio.play.ui.activities.MainActivity;
+import com.paulrybitskyi.persistentsearchview.utils.VoiceRecognitionDelegate;
+
+public class SearchFragment extends Fragment {
+ private static final String TAG = "SearchFragment";
+ public static final int REQUEST_CODE = 64545;
+
+ private FragmentSearchBinding bind;
+ private MainActivity activity;
+
+ protected LinearLayout emptyLinearLayout;
+
+ protected String query = "";
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ activity = (MainActivity) getActivity();
+
+ bind = FragmentSearchBinding.inflate(inflater, container, false);
+ View view = bind.getRoot();
+
+ searchInit();
+
+ return view;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ bind = null;
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == REQUEST_CODE) {
+ if (resultCode == Activity.RESULT_OK) {
+ String code = data.getStringExtra("result");
+ search(code);
+ }
+ }
+
+ VoiceRecognitionDelegate.handleResult(bind.persistentSearchView, requestCode, resultCode, data);
+ }
+
+ private void searchInit() {
+ bind.persistentSearchView.showRightButton();
+
+ bind.persistentSearchView.setOnSearchQueryChangeListener((searchView, oldQuery, newQuery) -> {
+ });
+
+ bind.persistentSearchView.setOnLeftBtnClickListener(view -> {
+ });
+
+ bind.persistentSearchView.setOnRightBtnClickListener(view -> {
+ });
+
+ bind.persistentSearchView.setVoiceRecognitionDelegate(new VoiceRecognitionDelegate(this));
+
+ bind.persistentSearchView.setOnSearchConfirmedListener((searchView, query) -> {
+ if (!query.equals("")) {
+ searchView.collapse();
+ search(query);
+ }
+ });
+
+ bind.persistentSearchView.setSuggestionsDisabled(true);
+ }
+
+ public void search(String query) {
+ emptyScreen();
+ this.query = query;
+
+ bind.persistentSearchView.setInputQuery(query);
+ performSearch(query);
+ }
+
+ private void performSearch(String query) {
+
+ }
+
+ private void loadMoreItemSearch(String query, int page) {
+ manageProgressBar(true);
+
+ }
+
+ private void emptyScreen() {
+ emptyLinearLayout.setVisibility(View.GONE);
+ }
+
+
+ private void manageProgressBar(boolean show) {
+ if (show) {
+ bind.persistentSearchView.hideLeftButton();
+ bind.persistentSearchView.showProgressBar(true);
+ } else {
+ bind.persistentSearchView.showLeftButton();
+ bind.persistentSearchView.hideProgressBar(true);
+ }
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/SettingsFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/SettingsFragment.java
new file mode 100644
index 00000000..f8fab89e
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/SettingsFragment.java
@@ -0,0 +1,28 @@
+package com.cappielloantonio.play.ui.fragment;
+
+import android.os.Bundle;
+
+import androidx.preference.ListPreference;
+import androidx.preference.PreferenceFragmentCompat;
+
+import com.cappielloantonio.play.R;
+import com.cappielloantonio.play.helper.ThemeHelper;
+
+public class SettingsFragment extends PreferenceFragmentCompat {
+ private static final String TAG = "SettingsFragment";
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ setPreferencesFromResource(R.xml.global_preferences, rootKey);
+
+ ListPreference themePreference = findPreference("themePref");
+ if (themePreference != null) {
+ themePreference.setOnPreferenceChangeListener(
+ (preference, newValue) -> {
+ String themeOption = (String) newValue;
+ ThemeHelper.applyTheme(themeOption);
+ return true;
+ });
+ }
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/SyncFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/SyncFragment.java
new file mode 100644
index 00000000..b217d277
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/SyncFragment.java
@@ -0,0 +1,206 @@
+package com.cappielloantonio.play.ui.fragment;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.cappielloantonio.play.databinding.FragmentSyncBinding;
+import com.cappielloantonio.play.interfaces.MediaCallback;
+import com.cappielloantonio.play.model.Album;
+import com.cappielloantonio.play.model.Artist;
+import com.cappielloantonio.play.model.Genre;
+import com.cappielloantonio.play.model.Playlist;
+import com.cappielloantonio.play.model.Song;
+import com.cappielloantonio.play.repository.AlbumRepository;
+import com.cappielloantonio.play.repository.ArtistRepository;
+import com.cappielloantonio.play.repository.GenreRepository;
+import com.cappielloantonio.play.repository.PlaylistRepository;
+import com.cappielloantonio.play.repository.SongRepository;
+import com.cappielloantonio.play.ui.activities.MainActivity;
+import com.cappielloantonio.play.util.PreferenceUtil;
+import com.cappielloantonio.play.util.SyncUtil;
+
+import org.jellyfin.apiclient.model.dto.BaseItemDto;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SyncFragment extends Fragment {
+ private static final String TAG = "SyncFragment";
+
+ private MainActivity activity;
+ private FragmentSyncBinding bind;
+
+ private ArrayList progressing;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ activity = (MainActivity) getActivity();
+
+ bind = FragmentSyncBinding.inflate(inflater, container, false);
+ View view = bind.getRoot();
+ syncLibraries();
+
+ return view;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ bind = null;
+ }
+
+ private void showProgressBar() {
+ bind.loadingProgressBar.setVisibility(View.VISIBLE);
+ }
+
+ private void syncLibraries() {
+ progressing = new ArrayList<>();
+
+ SyncUtil.getLibraries(requireContext(), new MediaCallback() {
+ @Override
+ public void onError(Exception exception) {
+ Log.e(TAG, "onError: " + exception.getMessage());
+ }
+
+ @Override
+ public void onLoadMedia(List> media) {
+ List libraries = (List) media;
+
+ for (BaseItemDto itemDto : libraries) {
+ if (itemDto.getCollectionType().equals("music"))
+ SyncUtil.musicLibrary = itemDto;
+ }
+
+ startSyncing();
+ }
+ });
+ }
+
+ private void startSyncing() {
+ showProgressBar();
+ syncAlbums();
+ syncArtists();
+ syncGenres();
+ syncPlaylist();
+ syncSongs();
+ }
+
+ private void syncAlbums() {
+ SyncUtil.getAlbums(requireContext(), new MediaCallback() {
+ @Override
+ public void onError(Exception exception) {
+ Log.e(TAG, "onError: " + exception.getMessage());
+ setProgress(false);
+ }
+
+ @Override
+ public void onLoadMedia(List> media) {
+ AlbumRepository repository = new AlbumRepository(activity.getApplication());
+ repository.insertAll((ArrayList) media);
+ setProgress(true);
+ }
+ });
+ }
+
+ private void syncArtists() {
+ SyncUtil.getArtists(requireContext(), new MediaCallback() {
+ @Override
+ public void onError(Exception exception) {
+ Log.e(TAG, "onError: " + exception.getMessage());
+ setProgress(false);
+ }
+
+ @Override
+ public void onLoadMedia(List> media) {
+ ArtistRepository repository = new ArtistRepository(activity.getApplication());
+ repository.insertAll((ArrayList) media);
+ setProgress(true);
+ }
+ });
+ }
+
+ private void syncGenres() {
+ SyncUtil.getGenres(requireContext(), new MediaCallback() {
+ @Override
+ public void onError(Exception exception) {
+ Log.e(TAG, "onError: " + exception.getMessage());
+ setProgress(false);
+ }
+
+ @Override
+ public void onLoadMedia(List> media) {
+ GenreRepository repository = new GenreRepository(activity.getApplication());
+ repository.insertAll((ArrayList) media);
+ setProgress(true);
+ }
+ });
+ }
+
+ private void syncPlaylist() {
+ SyncUtil.getPlaylists(requireContext(), new MediaCallback() {
+ @Override
+ public void onError(Exception exception) {
+ Log.e(TAG, "onError: " + exception.getMessage());
+ setProgress(false);
+ }
+
+ @Override
+ public void onLoadMedia(List> media) {
+ PlaylistRepository repository = new PlaylistRepository(activity.getApplication());
+ repository.insertAll((ArrayList) media);
+ setProgress(true);
+ }
+ });
+ }
+
+ private void syncSongs() {
+ SyncUtil.getSongs(requireContext(), new MediaCallback() {
+ @Override
+ public void onError(Exception exception) {
+ Log.e(TAG, "onError: " + exception.getMessage());
+ setProgress(false);
+ }
+
+ @Override
+ public void onLoadMedia(List> media) {
+ SongRepository repository = new SongRepository(activity.getApplication());
+ repository.insertAll((ArrayList) media);
+ setProgress(true);
+ }
+ });
+ }
+
+ private void setProgress(boolean step) {
+ if (step) {
+ progressing.add(25);
+ bind.loadingProgressBar.setProgress(bind.loadingProgressBar.getProgress() + 25, true);
+ } else {
+ progressing.add(0);
+ }
+
+ countProgress();
+ }
+
+ private void countProgress() {
+ if (progressing.size() == 5) {
+ if (bind.loadingProgressBar.getProgress() == 100)
+ terminate();
+ else
+ Toast.makeText(requireContext(), "Sync error", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void terminate() {
+ PreferenceUtil.getInstance(requireContext()).setSync(true);
+ activity.goToHome();
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/util/PreferenceUtil.java b/app/src/main/java/com/cappielloantonio/play/util/PreferenceUtil.java
new file mode 100644
index 00000000..c4bce9f3
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/util/PreferenceUtil.java
@@ -0,0 +1,85 @@
+package com.cappielloantonio.play.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.preference.PreferenceManager;
+
+import com.cappielloantonio.play.helper.ThemeHelper;
+
+public class PreferenceUtil {
+ public static final String SERVER = "server";
+ public static final String USER = "user";
+ public static final String TOKEN = "token";
+
+ public static final String SYNC = "sync";
+
+ public static final String HOST_URL = "host";
+ public static final String IMAGE_CACHE_SIZE = "image_cache_size";
+
+ private static PreferenceUtil sInstance;
+
+ private final SharedPreferences mPreferences;
+
+ private PreferenceUtil(final Context context) {
+ mPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ }
+
+ public static PreferenceUtil getInstance(final Context context) {
+ if (sInstance == null) {
+ sInstance = new PreferenceUtil(context.getApplicationContext());
+ }
+
+ return sInstance;
+ }
+
+ public String getTheme() { return mPreferences.getString("themePref", ThemeHelper.DEFAULT_MODE ); }
+
+ public String getServer() {
+ return mPreferences.getString(SERVER, "https://jellyfin.org");
+ }
+
+ public void setServer(String server) {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putString(SERVER, server);
+ editor.apply();
+ }
+
+ public String getUser() {
+ return mPreferences.getString(USER, "");
+ }
+
+ public void setUser(String user) {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putString(USER, user);
+ editor.apply();
+ }
+
+ public String getToken() {
+ return mPreferences.getString(TOKEN, "");
+ }
+
+ public void setToken(String token) {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putString(TOKEN, token);
+ editor.apply();
+ }
+
+ public Boolean getSync() {
+ return mPreferences.getBoolean(SYNC, false);
+ }
+
+ public void setSync(Boolean sync) {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putBoolean(SYNC, sync);
+ editor.apply();
+ }
+
+ public final String getHostUrl() {
+ return mPreferences.getString(HOST_URL, "undefined");
+ }
+
+ public final int getImageCacheSize() {
+ return Integer.parseInt(mPreferences.getString(IMAGE_CACHE_SIZE, "400000000"));
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/play/util/SyncUtil.java b/app/src/main/java/com/cappielloantonio/play/util/SyncUtil.java
new file mode 100644
index 00000000..9f0b1d98
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/util/SyncUtil.java
@@ -0,0 +1,177 @@
+package com.cappielloantonio.play.util;
+
+import android.content.Context;
+
+import com.cappielloantonio.play.App;
+import com.cappielloantonio.play.interfaces.MediaCallback;
+import com.cappielloantonio.play.model.Album;
+import com.cappielloantonio.play.model.Artist;
+import com.cappielloantonio.play.model.Genre;
+import com.cappielloantonio.play.model.Playlist;
+import com.cappielloantonio.play.model.Song;
+
+import org.jellyfin.apiclient.interaction.Response;
+import org.jellyfin.apiclient.model.dto.BaseItemDto;
+import org.jellyfin.apiclient.model.querying.ArtistsQuery;
+import org.jellyfin.apiclient.model.querying.ItemFields;
+import org.jellyfin.apiclient.model.querying.ItemQuery;
+import org.jellyfin.apiclient.model.querying.ItemsByNameQuery;
+import org.jellyfin.apiclient.model.querying.ItemsResult;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class SyncUtil {
+ public static BaseItemDto musicLibrary;
+
+ public static void getLibraries(Context context, MediaCallback callback) {
+ String id = App.getApiClientInstance(context).getCurrentUserId();
+
+ App.getApiClientInstance(context).GetUserViews(id, new Response() {
+ @Override
+ public void onResponse(ItemsResult result) {
+ List libraries = new ArrayList<>();
+ libraries.addAll(Arrays.asList(result.getItems()));
+
+ callback.onLoadMedia(libraries);
+ }
+
+ @Override
+ public void onError(Exception exception) {
+ exception.printStackTrace();
+ }
+ });
+ }
+
+ public static void getSongs(Context context, MediaCallback callback) {
+ ItemQuery query = new ItemQuery();
+
+ query.setIncludeItemTypes(new String[]{"Audio"});
+ query.setFields(new ItemFields[]{ItemFields.MediaSources});
+ query.setUserId(App.getApiClientInstance(context).getCurrentUserId());
+ query.setRecursive(true);
+ query.setParentId(musicLibrary.getId());
+
+ App.getApiClientInstance(context).GetItemsAsync(query, new Response() {
+ @Override
+ public void onResponse(ItemsResult result) {
+ ArrayList songs = new ArrayList<>();
+
+ for (BaseItemDto itemDto : result.getItems()) {
+ songs.add(new Song(itemDto));
+ }
+
+ callback.onLoadMedia(songs);
+ }
+
+ @Override
+ public void onError(Exception exception) {
+ callback.onError(exception);
+ }
+ });
+ }
+
+ public static void getAlbums(Context context, MediaCallback callback) {
+ ItemQuery query = new ItemQuery();
+
+ query.setIncludeItemTypes(new String[]{"MusicAlbum"});
+ query.setUserId(App.getApiClientInstance(context).getCurrentUserId());
+ query.setRecursive(true);
+ query.setParentId(musicLibrary.getId());
+
+ App.getApiClientInstance(context).GetItemsAsync(query, new Response() {
+ @Override
+ public void onResponse(ItemsResult result) {
+ List albums = new ArrayList<>();
+ for (BaseItemDto itemDto : result.getItems()) {
+ albums.add(new Album(itemDto));
+ }
+
+ callback.onLoadMedia(albums);
+ }
+
+ @Override
+ public void onError(Exception exception) {
+ exception.printStackTrace();
+ }
+ });
+ }
+
+ public static void getArtists(Context context, MediaCallback callback) {
+ ArtistsQuery query = new ArtistsQuery();
+
+ query.setFields(new ItemFields[]{ItemFields.Genres});
+ query.setUserId(App.getApiClientInstance(context).getCurrentUserId());
+ query.setRecursive(true);
+ query.setParentId(musicLibrary.getId());
+
+ App.getApiClientInstance(context).GetAlbumArtistsAsync(query, new Response() {
+ @Override
+ public void onResponse(ItemsResult result) {
+ List artists = new ArrayList<>();
+ for (BaseItemDto itemDto : result.getItems()) {
+ artists.add(new Artist(itemDto));
+ }
+
+ callback.onLoadMedia(artists);
+ }
+
+ @Override
+ public void onError(Exception exception) {
+ exception.printStackTrace();
+ }
+ });
+ }
+
+ public static void getPlaylists(Context context, MediaCallback callback) {
+ ItemQuery query = new ItemQuery();
+
+ query.setIncludeItemTypes(new String[]{"Playlist"});
+ query.setUserId(App.getApiClientInstance(context).getCurrentUserId());
+ query.setRecursive(true);
+ query.setParentId(musicLibrary.getId());
+
+ App.getApiClientInstance(context).GetItemsAsync(query, new Response() {
+ @Override
+ public void onResponse(ItemsResult result) {
+ List playlists = new ArrayList<>();
+ for (BaseItemDto itemDto : result.getItems()) {
+ playlists.add(new Playlist(itemDto));
+ }
+
+ callback.onLoadMedia(playlists);
+ }
+
+ @Override
+ public void onError(Exception exception) {
+ exception.printStackTrace();
+ }
+ });
+ }
+
+ public static void getGenres(Context context, MediaCallback callback) {
+ ItemsByNameQuery query = new ItemsByNameQuery();
+
+ query.setUserId(App.getApiClientInstance(context).getCurrentUserId());
+ query.setRecursive(true);
+ query.setParentId(musicLibrary.getId());
+
+ App.getApiClientInstance(context).GetGenresAsync(query, new Response() {
+ @Override
+ public void onResponse(ItemsResult result) {
+ List genres = new ArrayList<>();
+ for (BaseItemDto itemDto : result.getItems()) {
+ genres.add(new Genre(itemDto));
+ }
+
+ callback.onLoadMedia(genres);
+ }
+
+ @Override
+ public void onError(Exception exception) {
+ exception.printStackTrace();
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/viewmodel/HomeViewModel.java b/app/src/main/java/com/cappielloantonio/play/viewmodel/HomeViewModel.java
new file mode 100644
index 00000000..3d4438a7
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/viewmodel/HomeViewModel.java
@@ -0,0 +1,41 @@
+package com.cappielloantonio.play.viewmodel;
+
+import androidx.lifecycle.ViewModel;
+
+import com.cappielloantonio.play.model.Song;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class HomeViewModel extends ViewModel {
+
+ public List getDiscoverSongList() {
+ List discover_songs = new ArrayList<>();
+ discover_songs.add(new Song("Holiday", "American Idiot"));
+ discover_songs.add(new Song("Brioschi", "Stanza Singola"));
+ discover_songs.add(new Song("HappySad", "Ceri Singles"));
+ discover_songs.add(new Song("Falling back to Earth", "Haken"));
+
+ return discover_songs;
+ }
+
+ public List getRecentSongList() {
+ List recent_songs = new ArrayList<>();
+ recent_songs.add(new Song("Holiday", "American Idiot"));
+ recent_songs.add(new Song("Brioschi", "Stanza Singola"));
+ recent_songs.add(new Song("HappySad", "Ceri Singles"));
+ recent_songs.add(new Song("Falling back to Earth", "Haken"));
+
+ return recent_songs;
+ }
+
+ public List getMostPlayedSongList() {
+ List most_played_songs = new ArrayList<>();
+ most_played_songs.add(new Song("Holiday", "American Idiot"));
+ most_played_songs.add(new Song("Brioschi", "Stanza Singola"));
+ most_played_songs.add(new Song("HappySad", "Ceri Singles"));
+ most_played_songs.add(new Song("Falling back to Earth", "Haken"));
+
+ return most_played_songs;
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/viewmodel/LibraryViewModel.java b/app/src/main/java/com/cappielloantonio/play/viewmodel/LibraryViewModel.java
new file mode 100644
index 00000000..4cdaf630
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/viewmodel/LibraryViewModel.java
@@ -0,0 +1,77 @@
+package com.cappielloantonio.play.viewmodel;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+
+import com.cappielloantonio.play.model.Album;
+import com.cappielloantonio.play.model.Artist;
+import com.cappielloantonio.play.model.Genre;
+import com.cappielloantonio.play.model.Playlist;
+import com.cappielloantonio.play.repository.GenreRepository;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class LibraryViewModel extends AndroidViewModel {
+ private GenreRepository genreRepository;
+ private LiveData> allGenres;
+
+ public LibraryViewModel(@NonNull Application application) {
+ super(application);
+
+ genreRepository = new GenreRepository(application);
+ allGenres = genreRepository.getListLiveGenres();
+ }
+
+ public LiveData> getGenreList() {
+ return allGenres;
+ }
+
+ public ArrayList getAlbumSample() {
+ ArrayList albums = new ArrayList<>();
+ albums.add(new Album("1", "aaaa", 1, "1", "qqqq", "", ""));
+ albums.add(new Album("2", "ssss", 1, "2", "wwww", "", ""));
+ albums.add(new Album("3", "dddd", 1, "3", "eeee", "", ""));
+ albums.add(new Album("4", "ffff", 1, "4", "rrrr", "", ""));
+ albums.add(new Album("5", "gggg", 1, "5", "tttt", "", ""));
+ albums.add(new Album("6", "hhhh", 1, "6", "yyyy", "", ""));
+ albums.add(new Album("7", "jjjj", 1, "7", "uuuu", "", ""));
+ albums.add(new Album("8", "kkkk", 1, "8", "iiii", "", ""));
+ albums.add(new Album("9", "llll", 1, "9", "oooo", "", ""));
+
+ return albums;
+ }
+
+ public ArrayList getArtistSample() {
+ ArrayList artists = new ArrayList<>();
+ artists.add(new Artist("1", "dhgr", "", ""));
+ artists.add(new Artist("2", "kdnu", "", ""));
+ artists.add(new Artist("3", "wfty", "", ""));
+ artists.add(new Artist("4", "hfds", "", ""));
+ artists.add(new Artist("5", "jgab", "", ""));
+ artists.add(new Artist("6", "iudg", "", ""));
+ artists.add(new Artist("7", "istr", "", ""));
+ artists.add(new Artist("8", "dger", "", ""));
+ artists.add(new Artist("9", "jhjk", "", ""));
+
+ return artists;
+ }
+
+ public ArrayList getPlaylist() {
+ ArrayList playlists = new ArrayList<>();
+ playlists.add(new Playlist("1", "sdad", "", ""));
+ playlists.add(new Playlist("2", "rwef", "", ""));
+ playlists.add(new Playlist("3", "khjf", "", ""));
+ playlists.add(new Playlist("4", "thfd", "", ""));
+ playlists.add(new Playlist("5", "jhku", "", ""));
+ playlists.add(new Playlist("6", "tuid", "", ""));
+ playlists.add(new Playlist("7", "hfrt", "", ""));
+ playlists.add(new Playlist("8", "qedg", "", ""));
+ playlists.add(new Playlist("9", "tugh", "", ""));
+
+ return playlists;
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/play/viewmodel/SongViewModel.java b/app/src/main/java/com/cappielloantonio/play/viewmodel/SongViewModel.java
new file mode 100644
index 00000000..ee74622d
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/play/viewmodel/SongViewModel.java
@@ -0,0 +1,40 @@
+package com.cappielloantonio.play.viewmodel;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+
+import com.cappielloantonio.play.model.Song;
+import com.cappielloantonio.play.repository.SongRepository;
+
+import java.util.List;
+
+public class SongViewModel extends AndroidViewModel {
+ private SongRepository repository;
+ private LiveData> allSongs;
+
+ public SongViewModel(@NonNull Application application) {
+ super(application);
+
+ repository = new SongRepository(application);
+ allSongs = repository.getListLiveSongs();
+ }
+
+ public boolean exist(Song song) {
+ return repository.exist(song);
+ }
+
+ public void insert(Song song) {
+ repository.insert(song);
+ }
+
+ public void delete(Song song) {
+ repository.delete(song);
+ }
+
+ public LiveData> getAllSongs() {
+ return allSongs;
+ }
+}
diff --git a/app/src/main/res/drawable/bottom_nav_selector.xml b/app/src/main/res/drawable/bottom_nav_selector.xml
new file mode 100644
index 00000000..c69010c7
--- /dev/null
+++ b/app/src/main/res/drawable/bottom_nav_selector.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bottom_nav_shape.xml b/app/src/main/res/drawable/bottom_nav_shape.xml
new file mode 100644
index 00000000..1a94901f
--- /dev/null
+++ b/app/src/main/res/drawable/bottom_nav_shape.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml
new file mode 100644
index 00000000..16d6d37d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_close.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_favorites.xml b/app/src/main/res/drawable/ic_favorites.xml
new file mode 100644
index 00000000..e6646709
--- /dev/null
+++ b/app/src/main/res/drawable/ic_favorites.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_filter.xml b/app/src/main/res/drawable/ic_filter.xml
new file mode 100644
index 00000000..4a5ee6a7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_filter.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_graphic_eq.xml b/app/src/main/res/drawable/ic_graphic_eq.xml
new file mode 100644
index 00000000..3932127d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_graphic_eq.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml
new file mode 100644
index 00000000..ad6065c4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_home.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_qr_code.xml b/app/src/main/res/drawable/ic_qr_code.xml
new file mode 100644
index 00000000..fefd5c80
--- /dev/null
+++ b/app/src/main/res/drawable/ic_qr_code.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml
new file mode 100644
index 00000000..da931c27
--- /dev/null
+++ b/app/src/main/res/drawable/ic_search.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml
new file mode 100644
index 00000000..282c986f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_settings.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/font/bold.ttf b/app/src/main/res/font/bold.ttf
new file mode 100644
index 00000000..54e7059c
Binary files /dev/null and b/app/src/main/res/font/bold.ttf differ
diff --git a/app/src/main/res/font/open_sans_font_family.xml b/app/src/main/res/font/open_sans_font_family.xml
new file mode 100644
index 00000000..4fbd1655
--- /dev/null
+++ b/app/src/main/res/font/open_sans_font_family.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/font/regular.ttf b/app/src/main/res/font/regular.ttf
new file mode 100644
index 00000000..29bfd35a
Binary files /dev/null and b/app/src/main/res/font/regular.ttf differ
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..61a83162
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
new file mode 100644
index 00000000..f4fc4d84
--- /dev/null
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -0,0 +1,194 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_landing.xml b/app/src/main/res/layout/fragment_landing.xml
new file mode 100644
index 00000000..a667e6e0
--- /dev/null
+++ b/app/src/main/res/layout/fragment_landing.xml
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_library.xml b/app/src/main/res/layout/fragment_library.xml
new file mode 100644
index 00000000..3fe33968
--- /dev/null
+++ b/app/src/main/res/layout/fragment_library.xml
@@ -0,0 +1,262 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml
new file mode 100644
index 00000000..a6081a35
--- /dev/null
+++ b/app/src/main/res/layout/fragment_login.xml
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml
new file mode 100644
index 00000000..e48530e9
--- /dev/null
+++ b/app/src/main/res/layout/fragment_search.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml
new file mode 100644
index 00000000..c6e03e60
--- /dev/null
+++ b/app/src/main/res/layout/fragment_settings.xml
@@ -0,0 +1,6 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_sync.xml b/app/src/main/res/layout/fragment_sync.xml
new file mode 100644
index 00000000..985dc34e
--- /dev/null
+++ b/app/src/main/res/layout/fragment_sync.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_discover_song.xml b/app/src/main/res/layout/item_discover_song.xml
new file mode 100644
index 00000000..d0fe0fb9
--- /dev/null
+++ b/app/src/main/res/layout/item_discover_song.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_library_album.xml b/app/src/main/res/layout/item_library_album.xml
new file mode 100644
index 00000000..998da520
--- /dev/null
+++ b/app/src/main/res/layout/item_library_album.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_library_artist.xml b/app/src/main/res/layout/item_library_artist.xml
new file mode 100644
index 00000000..aac681ea
--- /dev/null
+++ b/app/src/main/res/layout/item_library_artist.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_library_genre.xml b/app/src/main/res/layout/item_library_genre.xml
new file mode 100644
index 00000000..ee8c3a1b
--- /dev/null
+++ b/app/src/main/res/layout/item_library_genre.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_library_playlist.xml b/app/src/main/res/layout/item_library_playlist.xml
new file mode 100644
index 00000000..b1ed9424
--- /dev/null
+++ b/app/src/main/res/layout/item_library_playlist.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_recent_track.xml b/app/src/main/res/layout/item_recent_track.xml
new file mode 100644
index 00000000..81823933
--- /dev/null
+++ b/app/src/main/res/layout/item_recent_track.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml
new file mode 100644
index 00000000..80b09a67
--- /dev/null
+++ b/app/src/main/res/menu/bottom_nav_menu.xml
@@ -0,0 +1,19 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..a6ac0a9d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..6bba2f5f
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..93e13869
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..24a9a7f2
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..9d33613a
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
new file mode 100644
index 00000000..ecd5b124
--- /dev/null
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night-v27/styles.xml b/app/src/main/res/values-night-v27/styles.xml
new file mode 100644
index 00000000..547cd0f1
--- /dev/null
+++ b/app/src/main/res/values-night-v27/styles.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
new file mode 100644
index 00000000..a455df34
--- /dev/null
+++ b/app/src/main/res/values-night/colors.xml
@@ -0,0 +1,37 @@
+
+
+ #121212
+ #121212
+ #733ae6
+ #aa6aff
+
+
+ #121212
+ #121212
+ #121211
+
+ #1D1D1D
+
+ #DADADA
+ #9B9B9B
+ #808080
+ #808080
+
+ #DADADA
+ #DADADA
+ #CFCFCF
+ #CFCFCF
+ #CCCCCC
+ #707070
+ #CFCFCF
+ #CFCFCF
+ #121212
+
+ #F3F3F3
+ #A0A0A0
+
+ #FFFFFF
+ #EEEEEE
+ #707070
+
+
diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml
new file mode 100644
index 00000000..cd3f043b
--- /dev/null
+++ b/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-v27/styles.xml b/app/src/main/res/values-v27/styles.xml
new file mode 100644
index 00000000..2dd53335
--- /dev/null
+++ b/app/src/main/res/values-v27/styles.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
new file mode 100644
index 00000000..d14d4a21
--- /dev/null
+++ b/app/src/main/res/values/arrays.xml
@@ -0,0 +1,134 @@
+
+
+
+ - Reply
+ - Reply to all
+
+
+
+ - reply
+ - reply_all
+
+
+
+ - Light
+ - Dark
+ - System default
+
+
+
+ - light
+ - dark
+ - default
+
+
+
+ - Title
+ - Author(s)
+ - Series
+ - Publisher
+ - Year
+ - ISBN
+ - Language
+ - Tags
+ - Extension
+ - All
+
+
+
+ - title
+ - author
+ - series
+ - publisher
+ - year
+ - isbn
+ - language
+ - tags
+ - extension
+ - all
+
+
+
+ - Yes
+ - No
+
+
+
+ - yes
+ - no
+
+
+
+ - 25
+ - 50
+
+
+
+ - 25
+ - 50
+
+
+
+ - Unlimited
+ - 2GB
+ - 1GB
+ - 400MB
+ - 200MB
+
+
+
+ - 4000000000
+ - 2000000000
+ - 1000000000
+ - 400000000
+ - 200000000
+
+
+
+ - Ascending
+ - Descending
+ - Default
+
+
+
+ - ASC
+ - DESC
+ - def
+
+
+
+ - ID
+ - Author(s)
+ - Author(s)
+ - Publisher
+ - Year
+ - Pages
+ - Language
+ - Size
+ - Extension
+ - None
+
+
+
+ - id
+ - author
+ - title
+ - publisher
+ - year
+ - pages
+ - language
+ - filesize
+ - extension
+ - none
+
+
+
+ - Yes
+ - No
+
+
+
+ - yes
+ - no
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..5df8f573
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,37 @@
+
+
+ #FFFFFF
+ #FFFFFF
+ #3700B3
+ #733ae6
+
+
+ #FFFFFF
+ #FFFFFF
+ #FFFFFF
+
+
+ #FFFFFF
+
+ #252525
+ #646464
+ #7f7f7f
+ #7f7f7f
+
+ #252525
+ #252525
+ #303030
+ #303030
+ #333333
+ #AFAFAF
+ #303030
+ #303030
+ #EEEEEE
+
+ #383838
+ #757575
+
+ #252525
+ #303030
+ #AFAFAF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..4ab4520f
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,8 @@
+
+
+ 16dp
+ 16dp
+ 8dp
+ 176dp
+ 16dp
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..0b6ac8e5
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,35 @@
+
+ Play
+ We need permissions because this and that
+ Settings
+ Filters
+ Details
+
+ Settings
+ General
+ Search title, artists or albums
+ Home
+ Settings
+ Download
+ Shelf
+ Theme
+ Choose theme
+ Covers cache
+ Search
+ Favorites
+ Categories
+ Library
+ Home
+ Search
+ --
+ Save filters between sessions
+
+ Battery Optimizations
+ Please disable battery optimizations for media playback while the screen is off.
+
+ Disable
+ Ignore
+
+ Hello blank fragment
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..50184eb4
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/global_preferences.xml b/app/src/main/res/xml/global_preferences.xml
new file mode 100644
index 00000000..9204ec7e
--- /dev/null
+++ b/app/src/main/res/xml/global_preferences.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/cappielloantonio/play/ExampleUnitTest.java b/app/src/test/java/com/cappielloantonio/play/ExampleUnitTest.java
new file mode 100644
index 00000000..a38a927b
--- /dev/null
+++ b/app/src/test/java/com/cappielloantonio/play/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.cappielloantonio.play;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 00000000..02ccbfba
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.1.0'
+ classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.10'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+
+ maven { url 'https://jitpack.io' }
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 00000000..c52ac9b7
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,19 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..f6b961fd
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..f013bf48
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Nov 04 18:13:42 CET 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
diff --git a/gradlew b/gradlew
new file mode 100644
index 00000000..cccdd3d5
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# 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
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# 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
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+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" -a "$nonstop" = "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
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 00000000..f9553162
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@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
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@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=
+
+@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 Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_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=%*
+
+: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/settings.gradle b/settings.gradle
new file mode 100644
index 00000000..b1d91ddf
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+include ':app'
+rootProject.name = "Play"
\ No newline at end of file