diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java index 9bf95803..3c90da4f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java @@ -4,7 +4,7 @@ import android.content.ComponentName; import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.os.Parcelable; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -12,6 +12,7 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; +import android.widget.ToggleButton; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -60,12 +61,14 @@ public class AlbumPageFragment extends Fragment implements ClickCallback { private SongHorizontalAdapter songHorizontalAdapter; private ListenableFuture mediaBrowserListenableFuture; + /** @noinspection deprecation*/ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); } + /** @noinspection deprecation*/ @Override public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); @@ -81,7 +84,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback { albumPageViewModel = new ViewModelProvider(requireActivity()).get(AlbumPageViewModel.class); playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); - init(); + init(view); initAppBar(); initAlbumInfoTextButton(); initAlbumNotes(); @@ -119,12 +122,13 @@ public class AlbumPageFragment extends Fragment implements ClickCallback { bind = null; } + /** @noinspection deprecation*/ @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == R.id.action_rate_album) { Bundle bundle = new Bundle(); AlbumID3 album = albumPageViewModel.getAlbum().getValue(); - bundle.putParcelable(Constants.ALBUM_OBJECT, (Parcelable) album); + bundle.putParcelable(Constants.ALBUM_OBJECT, album); RatingDialog dialog = new RatingDialog(); dialog.setArguments(bundle); dialog.show(requireActivity().getSupportFragmentManager(), null); @@ -159,8 +163,21 @@ public class AlbumPageFragment extends Fragment implements ClickCallback { return false; } - private void init() { - albumPageViewModel.setAlbum(getViewLifecycleOwner(), requireArguments().getParcelable(Constants.ALBUM_OBJECT)); + private void init(View view) { + AlbumID3 albumArg = requireArguments().getParcelable(Constants.ALBUM_OBJECT); + assert albumArg != null; + albumPageViewModel.setAlbum(getViewLifecycleOwner(), albumArg); + ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite); + favoriteToggle.setChecked(albumArg.getStarred() != null); + + favoriteToggle.setOnClickListener(v -> { + albumPageViewModel.setFavorite(); + }); + albumPageViewModel.getAlbum().observe(getViewLifecycleOwner(), album -> { + if (album != null) { + favoriteToggle.setChecked(album.getStarred() != null); + } + }); } private void initAppBar() { diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java index 2d39eea1..9fbce6dc 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java @@ -2,14 +2,18 @@ package com.cappielloantonio.tempo.ui.fragment; import android.content.ComponentName; import android.content.Intent; +import android.graphics.drawable.Drawable; import android.net.Uri; 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 android.widget.ToggleButton; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.media3.common.util.UnstableApi; @@ -40,6 +44,7 @@ import com.google.common.util.concurrent.ListenableFuture; import java.util.ArrayList; import java.util.List; +import java.util.Objects; @UnstableApi public class ArtistPageFragment extends Fragment implements ClickCallback { @@ -63,7 +68,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { artistPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistPageViewModel.class); playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); - init(); + init(view); initAppBar(); initArtistInfo(); initPlayButtons(); @@ -100,7 +105,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { bind = null; } - private void init() { + private void init(View view) { artistPageViewModel.setArtist(requireArguments().getParcelable(Constants.ARTIST_OBJECT)); bind.mostStreamedSongTextViewClickable.setOnClickListener(v -> { @@ -109,6 +114,10 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { bundle.putParcelable(Constants.ARTIST_OBJECT, artistPageViewModel.getArtist()); activity.navController.navigate(R.id.action_artistPageFragment_to_songListPageFragment, bundle); }); + + ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite); + favoriteToggle.setChecked(artistPageViewModel.getArtist().getStarred() != null); + favoriteToggle.setOnClickListener(v -> artistPageViewModel.setFavorite(requireContext())); } private void initAppBar() { @@ -133,10 +142,54 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { if (bind != null) bind.bioMoreTextViewClickable.setVisibility(artistInfo.getLastFmUrl() != null ? View.VISIBLE : View.GONE); - if (getContext() != null && bind != null) CustomGlideRequest.Builder - .from(requireContext(), artistPageViewModel.getArtist().getId(), CustomGlideRequest.ResourceType.Artist) - .build() - .into(bind.artistBackdropImageView); + if (getContext() != null && bind != null) { + ArtistID3 currentArtist = artistPageViewModel.getArtist(); + String primaryId = currentArtist.getCoverArtId() != null && !currentArtist.getCoverArtId().trim().isEmpty() + ? currentArtist.getCoverArtId() + : currentArtist.getId(); + + final String fallbackId = (Objects.requireNonNull(primaryId).equals(currentArtist.getCoverArtId()) && + currentArtist.getId() != null && + !currentArtist.getId().equals(primaryId)) + ? currentArtist.getId() + : null; + + CustomGlideRequest.Builder + .from(requireContext(), primaryId, CustomGlideRequest.ResourceType.Artist) + .build() + .listener(new com.bumptech.glide.request.RequestListener() { + @Override + public boolean onLoadFailed(@Nullable com.bumptech.glide.load.engine.GlideException e, + Object model, + @NonNull com.bumptech.glide.request.target.Target target, + boolean isFirstResource) { + if (e != null) { + e.getMessage(); + if (e.getMessage().contains("400") && fallbackId != null) { + + Log.d("ArtistCover", "Primary ID failed (400), trying fallback: " + fallbackId); + + CustomGlideRequest.Builder + .from(requireContext(), fallbackId, CustomGlideRequest.ResourceType.Artist) + .build() + .into(bind.artistBackdropImageView); + return true; + } + } + return false; + } + + @Override + public boolean onResourceReady(@NonNull Drawable resource, + @NonNull Object model, + com.bumptech.glide.request.target.Target target, + @NonNull com.bumptech.glide.load.DataSource dataSource, + boolean isFirstResource) { + return false; + } + }) + .into(bind.artistBackdropImageView); + } if (bind != null) bind.bioTextView.setText(normalizedBio); @@ -150,29 +203,24 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { } }); } - private void initPlayButtons() { - bind.artistPageShuffleButton.setOnClickListener(v -> { - artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), songs -> { - if (!songs.isEmpty()) { - MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); - activity.setBottomSheetInPeek(true); - } else { - Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_tracks), Toast.LENGTH_SHORT).show(); - } - }); - }); + bind.artistPageShuffleButton.setOnClickListener(v -> artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), songs -> { + if (!songs.isEmpty()) { + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + activity.setBottomSheetInPeek(true); + } else { + Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_tracks), Toast.LENGTH_SHORT).show(); + } + })); - bind.artistPageRadioButton.setOnClickListener(v -> { - artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), songs -> { - if (songs != null && !songs.isEmpty()) { - MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); - activity.setBottomSheetInPeek(true); - } else { - Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_radio), Toast.LENGTH_SHORT).show(); - } - }); - }); + bind.artistPageRadioButton.setOnClickListener(v -> artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), songs -> { + if (songs != null && !songs.isEmpty()) { + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + activity.setBottomSheetInPeek(true); + } else { + Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_radio), Toast.LENGTH_SHORT).show(); + } + })); } private void initTopSongsView() { diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumPageViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumPageViewModel.java index 0979f408..595e9a0f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumPageViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumPageViewModel.java @@ -8,18 +8,23 @@ import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; +import com.cappielloantonio.tempo.interfaces.StarCallback; import com.cappielloantonio.tempo.repository.AlbumRepository; import com.cappielloantonio.tempo.repository.ArtistRepository; +import com.cappielloantonio.tempo.repository.FavoriteRepository; import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.AlbumInfo; import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.util.NetworkUtil; +import java.util.Date; import java.util.List; public class AlbumPageViewModel extends AndroidViewModel { private final AlbumRepository albumRepository; private final ArtistRepository artistRepository; + private final FavoriteRepository favoriteRepository; private String albumId; private String artistId; private final MutableLiveData album = new MutableLiveData<>(null); @@ -29,6 +34,7 @@ public class AlbumPageViewModel extends AndroidViewModel { albumRepository = new AlbumRepository(); artistRepository = new ArtistRepository(); + favoriteRepository = new FavoriteRepository(); } public LiveData> getAlbumSongLiveList() { @@ -49,6 +55,61 @@ public class AlbumPageViewModel extends AndroidViewModel { }); } + public void setFavorite() { + AlbumID3 currentAlbum = album.getValue(); + if (currentAlbum == null) return; + + if (currentAlbum.getStarred() != null) { + if (NetworkUtil.isOffline()) { + removeFavoriteOffline(currentAlbum); + } else { + removeFavoriteOnline(currentAlbum); + } + } else { + if (NetworkUtil.isOffline()) { + setFavoriteOffline(currentAlbum); + } else { + setFavoriteOnline(currentAlbum); + } + } + } + + private void removeFavoriteOffline(AlbumID3 album) { + favoriteRepository.starLater(null, album.getId(), null, false); + album.setStarred(null); + this.album.postValue(album); + } + + private void removeFavoriteOnline(AlbumID3 album) { + favoriteRepository.unstar(null, album.getId(), null, new StarCallback() { + @Override + public void onError() { + favoriteRepository.starLater(null, album.getId(), null, false); + } + }); + + album.setStarred(null); + this.album.postValue(album); + } + + private void setFavoriteOffline(AlbumID3 album) { + favoriteRepository.starLater(null, album.getId(), null, true); + album.setStarred(new Date()); + this.album.postValue(album); + } + + private void setFavoriteOnline(AlbumID3 album) { + favoriteRepository.star(null, album.getId(), null, new StarCallback() { + @Override + public void onError() { + favoriteRepository.starLater(null, album.getId(), null, true); + } + }); + + album.setStarred(new Date()); + this.album.postValue(album); + } + public LiveData getArtist() { return artistRepository.getArtistInfo(artistId); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java index a389cf75..871565d0 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java @@ -1,23 +1,37 @@ package com.cappielloantonio.tempo.viewmodel; import android.app.Application; +import android.content.Context; +import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.OptIn; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; +import androidx.media3.common.util.UnstableApi; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.interfaces.StarCallback; import com.cappielloantonio.tempo.repository.AlbumRepository; import com.cappielloantonio.tempo.repository.ArtistRepository; +import com.cappielloantonio.tempo.repository.FavoriteRepository; import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.ArtistInfo2; import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.NetworkUtil; +import com.cappielloantonio.tempo.util.Preferences; +import java.util.Date; import java.util.List; +import java.util.stream.Collectors; public class ArtistPageViewModel extends AndroidViewModel { private final AlbumRepository albumRepository; private final ArtistRepository artistRepository; + private final FavoriteRepository favoriteRepository; private ArtistID3 artist; @@ -26,6 +40,7 @@ public class ArtistPageViewModel extends AndroidViewModel { albumRepository = new AlbumRepository(); artistRepository = new ArtistRepository(); + favoriteRepository = new FavoriteRepository(); } public LiveData> getAlbumList() { @@ -55,4 +70,71 @@ public class ArtistPageViewModel extends AndroidViewModel { public void setArtist(ArtistID3 artist) { this.artist = artist; } + + public void setFavorite(Context context) { + if (artist.getStarred() != null) { + if (NetworkUtil.isOffline()) { + removeFavoriteOffline(); + } else { + removeFavoriteOnline(); + } + } else { + if (NetworkUtil.isOffline()) { + setFavoriteOffline(); + } else { + setFavoriteOnline(context); + } + } + } + + private void removeFavoriteOffline() { + favoriteRepository.starLater(null, null, artist.getId(), false); + artist.setStarred(null); + } + + private void removeFavoriteOnline() { + favoriteRepository.unstar(null, null, artist.getId(), new StarCallback() { + @Override + public void onError() { + favoriteRepository.starLater(null, null, artist.getId(), false); + } + }); + + artist.setStarred(null); + } + + private void setFavoriteOffline() { + favoriteRepository.starLater(null, null, artist.getId(), true); + artist.setStarred(new Date()); + } + + private void setFavoriteOnline(Context context) { + favoriteRepository.star(null, null, artist.getId(), new StarCallback() { + @Override + public void onError() { + favoriteRepository.starLater(null, null, artist.getId(), true); + } + }); + + artist.setStarred(new Date()); + + if (Preferences.isStarredArtistsSyncEnabled()) { + artistRepository.getArtistAllSongs(artist.getId(), new ArtistRepository.ArtistSongsCallback() { + @OptIn(markerClass = UnstableApi.class) + @Override + public void onSongsCollected(List songs) { + if (songs != null && !songs.isEmpty()) { + DownloadUtil.getDownloadTracker(context).download( + MappingUtil.mapDownloads(songs), + songs.stream().map(Download::new).collect(Collectors.toList()) + ); + } else { + } + } + }); + } else { + Log.d("ArtistSync", "Artist sync preference is disabled"); + } + } + } diff --git a/app/src/main/res/layout/fragment_album_page.xml b/app/src/main/res/layout/fragment_album_page.xml index 1cf81bc3..3083415d 100644 --- a/app/src/main/res/layout/fragment_album_page.xml +++ b/app/src/main/res/layout/fragment_album_page.xml @@ -174,7 +174,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/album_notes_textview" /> - -