diff --git a/app/build.gradle b/app/build.gradle index c0c15a65..29a5acd5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -63,6 +63,7 @@ dependencies { // SearchBar implementation "com.paulrybitskyi.persistentsearchview:persistentsearchview:1.1.3" + implementation "com.arthurivanets.adapster:adapster:1.0.13" // Permission implementation 'pub.devrel:easypermissions:3.0.0' diff --git a/app/src/main/java/com/cappielloantonio/play/adapter/DiscoverSongAdapter.java b/app/src/main/java/com/cappielloantonio/play/adapter/DiscoverSongAdapter.java index c759750f..a194540c 100644 --- a/app/src/main/java/com/cappielloantonio/play/adapter/DiscoverSongAdapter.java +++ b/app/src/main/java/com/cappielloantonio/play/adapter/DiscoverSongAdapter.java @@ -6,68 +6,69 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import androidx.viewpager.widget.PagerAdapter; import com.cappielloantonio.play.App; import com.cappielloantonio.play.R; import com.cappielloantonio.play.model.Song; import com.cappielloantonio.play.repository.SongRepository; -import com.cappielloantonio.play.util.Util; import java.util.List; -public class DiscoverSongAdapter extends PagerAdapter { +public class DiscoverSongAdapter extends RecyclerView.Adapter { private static final String TAG = "DiscoverSongAdapter"; private List songs; private LayoutInflater layoutInflater; private Context context; - private View view; public DiscoverSongAdapter(Context context, List songs) { this.context = context; + this.layoutInflater = LayoutInflater.from(context); this.songs = songs; } @Override - public int getCount() { + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = layoutInflater.inflate(R.layout.item_home_discover_song, 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.textAlbum.setText(song.getAlbumName()); + } + + @Override + public int getItemCount() { return songs.size(); } - @Override - public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { - return view.equals(object); - } + public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + TextView textTitle; + TextView textAlbum; - @NonNull - @Override - public Object instantiateItem(@NonNull ViewGroup container, final int position) { - layoutInflater = LayoutInflater.from(context); - view = layoutInflater.inflate(R.layout.item_home_discover_song, container, false); + ViewHolder(View itemView) { + super(itemView); - TextView title = view.findViewById(R.id.title_discover_song_label); - TextView desc = view.findViewById(R.id.album_discover_song_label); - title.setText(songs.get(position).getTitle()); - desc.setText(songs.get(position).getAlbumName()); + textTitle = itemView.findViewById(R.id.title_discover_song_label); + textAlbum = itemView.findViewById(R.id.album_discover_song_label); - view.setOnClickListener(v -> { + itemView.setOnClickListener(this); + } + + @Override + public void onClick(View view) { SongRepository songRepository = new SongRepository(App.getInstance()); - songRepository.update(songs.get(position)); - }); - - container.addView(view, 0); - return view; + songRepository.update(songs.get(getAdapterPosition())); + } } public void setItems(List songs) { this.songs = songs; notifyDataSetChanged(); } - - @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/RecentMusicAdapter.java b/app/src/main/java/com/cappielloantonio/play/adapter/RecentMusicAdapter.java index 96edf380..f9fb27e2 100644 --- a/app/src/main/java/com/cappielloantonio/play/adapter/RecentMusicAdapter.java +++ b/app/src/main/java/com/cappielloantonio/play/adapter/RecentMusicAdapter.java @@ -10,10 +10,8 @@ import androidx.recyclerview.widget.RecyclerView; import com.cappielloantonio.play.App; import com.cappielloantonio.play.R; -import com.cappielloantonio.play.model.Artist; import com.cappielloantonio.play.model.Song; import com.cappielloantonio.play.repository.SongRepository; -import com.cappielloantonio.play.util.Util; import java.util.List; @@ -43,7 +41,7 @@ public class RecentMusicAdapter extends RecyclerView.Adapter searchSuggestions(String query, int number); } \ 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 index 44bc669f..77a207f7 100644 --- a/app/src/main/java/com/cappielloantonio/play/database/dao/ArtistDao.java +++ b/app/src/main/java/com/cappielloantonio/play/database/dao/ArtistDao.java @@ -33,4 +33,7 @@ public interface ArtistDao { @Delete void delete(Artist artist); + + @Query("SELECT name FROM artist WHERE name LIKE :query || '%' GROUP BY name LIMIT :number") + List searchSuggestions(String query, int number); } \ 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 index 5f91eb20..028c14c0 100644 --- a/app/src/main/java/com/cappielloantonio/play/database/dao/SongDao.java +++ b/app/src/main/java/com/cappielloantonio/play/database/dao/SongDao.java @@ -65,4 +65,7 @@ public interface SongDao { @Query("SELECT * FROM song ORDER BY RANDOM() LIMIT :number") List random(int number); + + @Query("SELECT title FROM song WHERE title LIKE :query || '%' GROUP BY title LIMIT :number") + List searchSuggestions(String query, int number); } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/play/repository/AlbumRepository.java b/app/src/main/java/com/cappielloantonio/play/repository/AlbumRepository.java index 94c9106d..5783abf1 100644 --- a/app/src/main/java/com/cappielloantonio/play/repository/AlbumRepository.java +++ b/app/src/main/java/com/cappielloantonio/play/repository/AlbumRepository.java @@ -1,19 +1,24 @@ package com.cappielloantonio.play.repository; import android.app.Application; +import android.util.Log; import androidx.lifecycle.LiveData; import com.cappielloantonio.play.database.AppDatabase; import com.cappielloantonio.play.database.dao.AlbumDao; +import com.cappielloantonio.play.database.dao.SongDao; import com.cappielloantonio.play.model.Album; import com.cappielloantonio.play.model.Artist; import com.cappielloantonio.play.model.Song; +import com.paulrybitskyi.persistentsearchview.adapters.model.SuggestionItem; import java.util.ArrayList; import java.util.List; public class AlbumRepository { + private static final String TAG = "AlbumRepository"; + private AlbumDao albumDao; private LiveData> listLiveAlbums; private LiveData> artistListLiveAlbums; @@ -46,6 +51,23 @@ public class AlbumRepository { return searchListLiveAlbum; } + public List getSearchSuggestion(String query) { + List suggestions = new ArrayList<>(); + + SearchSuggestionsThreadSafe suggestionsThread = new SearchSuggestionsThreadSafe(albumDao, query, 5); + Thread thread = new Thread(suggestionsThread); + thread.start(); + + try { + thread.join(); + suggestions = suggestionsThread.getSuggestions(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return suggestions; + } + public boolean exist(Album album) { boolean exist = false; @@ -145,4 +167,26 @@ public class AlbumRepository { albumDao.delete(album); } } + + private static class SearchSuggestionsThreadSafe implements Runnable { + private AlbumDao albumDao; + private String query; + private int number; + private List suggestions = new ArrayList<>(); + + public SearchSuggestionsThreadSafe(AlbumDao albumDao, String query, int number) { + this.albumDao = albumDao; + this.query = query; + this.number = number; + } + + @Override + public void run() { + suggestions = albumDao.searchSuggestions(query, number); + } + + public List getSuggestions() { + return suggestions; + } + } } diff --git a/app/src/main/java/com/cappielloantonio/play/repository/ArtistRepository.java b/app/src/main/java/com/cappielloantonio/play/repository/ArtistRepository.java index 60aec4ec..b3f1ebee 100644 --- a/app/src/main/java/com/cappielloantonio/play/repository/ArtistRepository.java +++ b/app/src/main/java/com/cappielloantonio/play/repository/ArtistRepository.java @@ -5,12 +5,14 @@ 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.database.dao.ArtistDao; import com.cappielloantonio.play.model.Album; import com.cappielloantonio.play.model.Artist; import com.cappielloantonio.play.model.Song; import java.util.ArrayList; +import java.util.Collection; import java.util.List; public class ArtistRepository { @@ -40,6 +42,23 @@ public class ArtistRepository { return searchListLiveArtist; } + public List getSearchSuggestion(String query) { + List suggestions = new ArrayList<>(); + + SearchSuggestionsThreadSafe suggestionsThread = new SearchSuggestionsThreadSafe(artistDao, query, 5); + Thread thread = new Thread(suggestionsThread); + thread.start(); + + try { + thread.join(); + suggestions = suggestionsThread.getSuggestions(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return suggestions; + } + public boolean exist(Artist artist) { boolean exist = false; @@ -140,4 +159,26 @@ public class ArtistRepository { artistDao.delete(artist); } } + + private static class SearchSuggestionsThreadSafe implements Runnable { + private ArtistDao artistDao; + private String query; + private int number; + private List suggestions = new ArrayList<>(); + + public SearchSuggestionsThreadSafe(ArtistDao artistDao, String query, int number) { + this.artistDao = artistDao; + this.query = query; + this.number = number; + } + + @Override + public void run() { + suggestions = artistDao.searchSuggestions(query, number); + } + + public List getSuggestions() { + return suggestions; + } + } } diff --git a/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java b/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java index 8a3e25e4..319ece74 100644 --- a/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/play/repository/SongRepository.java @@ -5,11 +5,13 @@ 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.database.dao.SongDao; import com.cappielloantonio.play.model.Album; import com.cappielloantonio.play.model.Song; import java.util.ArrayList; +import java.util.Collection; import java.util.List; public class SongRepository { @@ -74,6 +76,23 @@ public class SongRepository { return listLiveFilteredSongs; } + public List getSearchSuggestion(String query) { + List suggestions = new ArrayList<>(); + + SearchSuggestionsThreadSafe suggestionsThread = new SearchSuggestionsThreadSafe(songDao, query, 5); + Thread thread = new Thread(suggestionsThread); + thread.start(); + + try { + thread.join(); + suggestions = suggestionsThread.getSuggestions(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return suggestions; + } + public boolean exist(Song song) { boolean exist = false; @@ -233,4 +252,26 @@ public class SongRepository { return sample; } } + + private static class SearchSuggestionsThreadSafe implements Runnable { + private SongDao songDao; + private String query; + private int number; + private List suggestions = new ArrayList<>(); + + public SearchSuggestionsThreadSafe(SongDao songDao, String query, int number) { + this.songDao = songDao; + this.query = query; + this.number = number; + } + + @Override + public void run() { + suggestions = songDao.searchSuggestions(query, number); + } + + public List getSuggestions() { + return suggestions; + } + } } 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 index 09da26ed..e539faf9 100644 --- a/app/src/main/java/com/cappielloantonio/play/ui/fragment/HomeFragment.java +++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/HomeFragment.java @@ -7,9 +7,11 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.viewpager2.widget.ViewPager2; import com.cappielloantonio.play.R; import com.cappielloantonio.play.adapter.DiscoverSongAdapter; @@ -85,9 +87,12 @@ public class HomeFragment extends Fragment { } private void initDiscoverSongSlideView() { + bind.discoverSongViewPager.setOrientation(ViewPager2.ORIENTATION_HORIZONTAL); + discoverSongAdapter = new DiscoverSongAdapter(requireContext(), homeViewModel.getDiscoverSongList()); bind.discoverSongViewPager.setAdapter(discoverSongAdapter); - bind.discoverSongViewPager.setPageMargin(20); + bind.discoverSongViewPager.setOffscreenPageLimit(3); + settDiscoverSongSlideViewOffset(20, 16); } private void initRecentAddedSongView() { @@ -116,4 +121,19 @@ public class HomeFragment extends Fragment { bind.mostPlayedTracksRecyclerView.setAdapter(mostPlayedMusicAdapter); homeViewModel.getMostPlayedSongList().observe(requireActivity(), songs -> mostPlayedMusicAdapter.setItems(songs)); } + + private void settDiscoverSongSlideViewOffset(float pageOffset, float pageMargin) { + bind.discoverSongViewPager.setPageTransformer((page, position) -> { + float myOffset = position * -(2 * pageOffset + pageMargin); + if (bind.discoverSongViewPager.getOrientation() == ViewPager2.ORIENTATION_HORIZONTAL) { + if (ViewCompat.getLayoutDirection(bind.discoverSongViewPager) == ViewCompat.LAYOUT_DIRECTION_RTL) { + page.setTranslationX(-myOffset); + } else { + page.setTranslationX(myOffset); + } + } else { + page.setTranslationY(myOffset); + } + }); + } } 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 index 57432bf1..20643489 100644 --- a/app/src/main/java/com/cappielloantonio/play/ui/fragment/SearchFragment.java +++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/SearchFragment.java @@ -4,13 +4,11 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.LinearLayout; 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; @@ -22,14 +20,14 @@ import com.cappielloantonio.play.adapter.RecentSearchAdapter; import com.cappielloantonio.play.adapter.SongResultSearchAdapter; import com.cappielloantonio.play.databinding.FragmentSearchBinding; import com.cappielloantonio.play.helper.recyclerview.ItemlDecoration; -import com.cappielloantonio.play.model.Artist; import com.cappielloantonio.play.model.RecentSearch; -import com.cappielloantonio.play.model.Song; import com.cappielloantonio.play.ui.activities.MainActivity; import com.cappielloantonio.play.viewmodel.SearchViewModel; +import com.paulrybitskyi.persistentsearchview.adapters.model.SuggestionItem; +import com.paulrybitskyi.persistentsearchview.listeners.OnSuggestionChangeListener; +import com.paulrybitskyi.persistentsearchview.utils.SuggestionCreationUtil; import java.util.ArrayList; -import java.util.List; public class SearchFragment extends Fragment { private static final String TAG = "SearchFragment"; @@ -120,12 +118,23 @@ public class SearchFragment extends Fragment { } private void initSearchView() { - bind.persistentSearchView.showRightButton(); - bind.persistentSearchView.setOnSearchQueryChangeListener((searchView, oldQuery, newQuery) -> { + if (!newQuery.trim().equals("") && newQuery.trim().length() > 1) { + searchView.setSuggestions(SuggestionCreationUtil.asRegularSearchSuggestions(searchViewModel.getSearchSuggestion(newQuery)), false); + } else { + searchView.setSuggestions(new ArrayList<>()); + } }); - bind.persistentSearchView.setOnLeftBtnClickListener(view -> { + bind.persistentSearchView.setOnSuggestionChangeListener(new OnSuggestionChangeListener() { + @Override + public void onSuggestionPicked(SuggestionItem suggestion) { + search(suggestion.getItemModel().getText()); + } + + @Override + public void onSuggestionRemoved(SuggestionItem suggestion) { + } }); bind.persistentSearchView.setOnSearchConfirmedListener((searchView, query) -> { @@ -134,12 +143,14 @@ public class SearchFragment extends Fragment { } public void search(String query) { - if (!query.equals("")) { + if (!query.trim().equals("") && query.trim().length() > 1) { searchViewModel.insertNewSearch(query); bind.persistentSearchView.collapse(); bind.persistentSearchView.setInputQuery(query); performSearch(query.trim()); + } else { + Toast.makeText(requireContext(), "Enter at least two characters", Toast.LENGTH_SHORT).show(); } } diff --git a/app/src/main/java/com/cappielloantonio/play/viewmodel/SearchViewModel.java b/app/src/main/java/com/cappielloantonio/play/viewmodel/SearchViewModel.java index 4d3fca37..96ac25dc 100644 --- a/app/src/main/java/com/cappielloantonio/play/viewmodel/SearchViewModel.java +++ b/app/src/main/java/com/cappielloantonio/play/viewmodel/SearchViewModel.java @@ -1,6 +1,7 @@ package com.cappielloantonio.play.viewmodel; import android.app.Application; +import android.util.Log; import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; @@ -16,10 +17,14 @@ import com.cappielloantonio.play.repository.ArtistRepository; import com.cappielloantonio.play.repository.GenreRepository; import com.cappielloantonio.play.repository.RecentSearchRepository; import com.cappielloantonio.play.repository.SongRepository; +import com.paulrybitskyi.persistentsearchview.adapters.model.SuggestionItem; +import java.util.ArrayList; import java.util.List; public class SearchViewModel extends AndroidViewModel { + private static final String TAG = "SearchViewModel"; + private SongRepository songRepository; private AlbumRepository albumRepository; private ArtistRepository artistRepository; @@ -66,4 +71,13 @@ public class SearchViewModel extends AndroidViewModel { public void deleteAllRecentSearch() { recentSearchRepository.deleteAll(); } + + public List getSearchSuggestion(String query) { + ArrayList suggestions = new ArrayList<>(); + suggestions.addAll(songRepository.getSearchSuggestion(query)); + suggestions.addAll(albumRepository.getSearchSuggestion(query)); + suggestions.addAll(artistRepository.getSearchSuggestion(query)); + + return suggestions; + } } diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index c4ecaf2f..c8ac0cfd 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -30,29 +30,13 @@ android:textStyle="bold" /> - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 7ea75502..9a374de3 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -11,7 +11,7 @@ android:paddingStart="8dp" android:paddingTop="8dp" android:paddingEnd="8dp" - app:areSuggestionsDisabled="true" + app:areSuggestionsDisabled="false" app:cardBackgroundColor="@color/cardColor" app:cardCornerRadius="4dp" app:cardElevation="2dp" @@ -28,7 +28,13 @@ app:queryInputHint="@string/search_hint" app:queryInputHintColor="@color/hintTextColor" app:queryInputTextColor="@color/hintTextColor" - app:shouldDimBehind="false" /> + app:shouldDimBehind="true" + + app:suggestionIconColor="@color/suggestionIconColor" + app:suggestionRecentSearchIconColor="@color/suggestionIconColor" + app:suggestionSearchSuggestionIconColor="@color/suggestionIconColor" + app:suggestionTextColor="@color/suggestionTextColor" + app:suggestionSelectedTextColor="@color/suggestionSelectedTextColor" /> + app:cardCornerRadius="4dp" + android:layout_marginTop="8dp" + android:layout_marginBottom="16dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp"> Settings General - Search + Search title, artists or albums Home Settings Download