diff --git a/app/build.gradle b/app/build.gradle index 5926fa3f..1deaecca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { minSdkVersion 24 targetSdk 35 - versionCode 29 - versionName '3.13.0' + versionCode 30 + versionName '3.14.1' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java index 085b2d17..c959648b 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java @@ -316,6 +316,7 @@ public class MainActivity extends BaseActivity { Preferences.setSkipSilenceMode(false); Preferences.setDataSavingMode(false); Preferences.setStarredSyncEnabled(false); + Preferences.setStarredAlbumsSyncEnabled(false); } private void resetMusicSession() { diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistChooserDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistChooserDialog.java index 5c0b119c..360a5ec5 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistChooserDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistChooserDialog.java @@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.ui.dialog; import android.app.Dialog; import android.os.Bundle; import android.view.View; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; @@ -97,8 +98,12 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba @Override public void onPlaylistClick(Bundle bundle) { - Playlist playlist = bundle.getParcelable(Constants.PLAYLIST_OBJECT); - playlistChooserViewModel.addSongsToPlaylist(playlist.getId()); - dismiss(); + if (playlistChooserViewModel.getSongsToAdd() != null && !playlistChooserViewModel.getSongsToAdd().isEmpty()) { + Playlist playlist = bundle.getParcelable(Constants.PLAYLIST_OBJECT); + playlistChooserViewModel.addSongsToPlaylist(playlist.getId()); + dismiss(); + } else { + Toast.makeText(requireContext(), R.string.playlist_chooser_dialog_toast_add_failure, Toast.LENGTH_SHORT).show(); + } } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RatingDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RatingDialog.java index fc829152..a2d16003 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RatingDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RatingDialog.java @@ -63,7 +63,11 @@ public class RatingDialog extends DialogFragment { bind.ratingBar.setRating(song.getUserRating() != null ? song.getUserRating() : 0); }); } else if (ratingViewModel.getAlbum() != null) { - ratingViewModel.getLiveAlbum().observe(this, album -> bind.ratingBar.setRating(/*album.getRating()*/ 0)); + ratingViewModel.getLiveAlbum().observe(this, album -> { + if (album != null) { + bind.ratingBar.setRating(album.getUserRating() != null ? album.getUserRating() : 0); + } + }); } else if (ratingViewModel.getArtist() != null) { ratingViewModel.getLiveArtist().observe(this, artist -> bind.ratingBar.setRating(/*artist.getRating()*/ 0)); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/StarredAlbumSyncDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/StarredAlbumSyncDialog.java new file mode 100644 index 00000000..6e6ef303 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/StarredAlbumSyncDialog.java @@ -0,0 +1,81 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.widget.Button; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogStarredSyncBinding; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.StarredAlbumsSyncViewModel; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.stream.Collectors; + +@OptIn(markerClass = UnstableApi.class) +public class StarredAlbumSyncDialog extends DialogFragment { + private StarredAlbumsSyncViewModel starredAlbumsSyncViewModel; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + DialogStarredSyncBinding bind = DialogStarredSyncBinding.inflate(getLayoutInflater()); + + starredAlbumsSyncViewModel = new ViewModelProvider(requireActivity()).get(StarredAlbumsSyncViewModel.class); + + return new MaterialAlertDialogBuilder(getActivity()) + .setView(bind.getRoot()) + .setTitle(R.string.starred_album_sync_dialog_title) + .setPositiveButton(R.string.starred_sync_dialog_positive_button, null) + .setNeutralButton(R.string.starred_sync_dialog_neutral_button, null) + .setNegativeButton(R.string.starred_sync_dialog_negative_button, null) + .create(); + } + + @Override + public void onResume() { + super.onResume(); + setButtonAction(requireContext()); + } + + private void setButtonAction(Context context) { + androidx.appcompat.app.AlertDialog dialog = (androidx.appcompat.app.AlertDialog) getDialog(); + + if (dialog != null) { + Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE); + positiveButton.setOnClickListener(v -> { + starredAlbumsSyncViewModel.getStarredAlbumSongs(requireActivity()).observe(this, allSongs -> { + if (allSongs != null && !allSongs.isEmpty()) { + DownloadUtil.getDownloadTracker(context).download( + MappingUtil.mapDownloads(allSongs), + allSongs.stream().map(Download::new).collect(Collectors.toList()) + ); + } + dialog.dismiss(); + }); + }); + + Button neutralButton = dialog.getButton(Dialog.BUTTON_NEUTRAL); + neutralButton.setOnClickListener(v -> { + Preferences.setStarredAlbumsSyncEnabled(true); + dialog.dismiss(); + }); + + Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE); + negativeButton.setOnClickListener(v -> { + Preferences.setStarredAlbumsSyncEnabled(false); + dialog.dismiss(); + }); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumListPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumListPageFragment.java index eb5b19c5..9a1557a6 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumListPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumListPageFragment.java @@ -145,7 +145,7 @@ public class AlbumListPageFragment extends Fragment implements ClickCallback { @Override public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { - inflater.inflate(R.menu.toolbar_menu, menu); + inflater.inflate(R.menu.artist_list_menu, menu); MenuItem searchItem = menu.findItem(R.id.action_search); 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 dc02dcbe..03e71e10 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,6 +4,7 @@ import android.content.ComponentName; import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.os.Parcelable; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -27,11 +28,13 @@ import com.cappielloantonio.tempo.databinding.FragmentAlbumPageBinding; import com.cappielloantonio.tempo.glide.CustomGlideRequest; import com.cappielloantonio.tempo.interfaces.ClickCallback; import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog; +import com.cappielloantonio.tempo.ui.dialog.RatingDialog; import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.MappingUtil; @@ -104,6 +107,16 @@ public class AlbumPageFragment extends Fragment implements ClickCallback { @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); + RatingDialog dialog = new RatingDialog(); + dialog.setArguments(bundle); + dialog.show(requireActivity().getSupportFragmentManager(), null); + return true; + } + if (item.getItemId() == R.id.action_download_album) { albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> { DownloadUtil.getDownloadTracker(requireContext()).download(MappingUtil.mapDownloads(songs), songs.stream().map(Download::new).collect(Collectors.toList())); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistCatalogueFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistCatalogueFragment.java index 1ad88e8e..17d01f95 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistCatalogueFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistCatalogueFragment.java @@ -13,6 +13,7 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.PopupMenu; import android.widget.SearchView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -24,6 +25,8 @@ import androidx.navigation.Navigation; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import android.util.Log; + import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.databinding.FragmentArtistCatalogueBinding; import com.cappielloantonio.tempo.helper.recyclerview.GridItemDecoration; @@ -32,6 +35,10 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter; import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.viewmodel.ArtistCatalogueViewModel; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; + +import java.util.ArrayList; +import java.util.List; @UnstableApi public class ArtistCatalogueFragment extends Fragment implements ClickCallback { @@ -125,23 +132,50 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback { SearchView searchView = (SearchView) searchItem.getActionView(); searchView.setImeOptions(EditorInfo.IME_ACTION_DONE); + + searchView.setQueryHint(getString(R.string.filter_artist)); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { - searchView.clearFocus(); - return false; + // this toast may be overkill... + Toast.makeText(requireContext(), "Search: " + query, Toast.LENGTH_SHORT).show(); + filterArtists(query); + return true; } @Override public boolean onQueryTextChange(String newText) { - artistAdapter.getFilter().filter(newText); - return false; + filterArtists(newText); + return true; } }); searchView.setPadding(-32, 0, 0, 0); } + private void filterArtists(String query) { + List allArtists = artistCatalogueViewModel.getArtistList().getValue(); + + if (allArtists == null || allArtists.isEmpty()) { + return; + } + + if (query == null || query.trim().isEmpty()) { + artistAdapter.setItems(allArtists); + } else { + String searchQuery = query.toLowerCase().trim(); + List filteredArtists = new ArrayList<>(); + + for (ArtistID3 artist : allArtists) { + if (artist.getName() != null && + artist.getName().toLowerCase().contains(searchQuery)) { + filteredArtists.add(artist); + } + } + artistAdapter.setItems(filteredArtists); + } + } + private void hideKeyboard(View view) { InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(view.getWindowToken(), 0); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java index b71c27e8..4d30ce30 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java @@ -39,6 +39,7 @@ import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Share; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter; import com.cappielloantonio.tempo.ui.adapter.AlbumHorizontalAdapter; @@ -111,6 +112,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { super.onViewCreated(view, savedInstanceState); initSyncStarredView(); + initSyncStarredAlbumsView(); initDiscoverSongSlideView(); initSimilarSongView(); initArtistRadio(); @@ -314,6 +316,63 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { }); } + private void initSyncStarredAlbumsView() { + if (Preferences.isStarredAlbumsSyncEnabled()) { + homeViewModel.getStarredAlbums(getViewLifecycleOwner()).observeForever(new Observer>() { + @Override + public void onChanged(List albums) { + if (albums != null) { + DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext()); + List albumsToSync = new ArrayList<>(); + int albumCount = 0; + + for (AlbumID3 album : albums) { + boolean needsSync = false; + albumCount++; + albumsToSync.add(album.getName()); + } + + if (albumCount > 0) { + bind.homeSyncStarredAlbumsCard.setVisibility(View.VISIBLE); + String message = getResources().getQuantityString( + R.plurals.home_sync_starred_albums_count, + albumCount, + albumCount + ); + bind.homeSyncStarredAlbumsToSync.setText(message); + } + } + + homeViewModel.getStarredAlbums(getViewLifecycleOwner()).removeObserver(this); + } + }); + } + + bind.homeSyncStarredAlbumsCancel.setOnClickListener(v -> { + bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE); + }); + + bind.homeSyncStarredAlbumsDownload.setOnClickListener(v -> { + homeViewModel.getAllStarredAlbumSongs().observeForever(new Observer>() { + @Override + public void onChanged(List allSongs) { + if (allSongs != null) { + DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext()); + + for (Child song : allSongs) { + if (!manager.isDownloaded(song.getId())) { + manager.download(MappingUtil.mapDownload(song), new Download(song)); + } + } + } + + homeViewModel.getAllStarredAlbumSongs().removeObserver(this); + bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE); + } + }); + }); + } + private void initDiscoverSongSlideView() { if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_DISCOVERY)) return; diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java index a07d58ec..99f3c4ca 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java @@ -8,6 +8,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageButton; +import android.widget.LinearLayout; import android.widget.TextView; import android.widget.ToggleButton; import android.widget.RatingBar; @@ -66,6 +67,7 @@ public class PlayerControllerFragment extends Fragment { private ConstraintLayout playerQuickActionView; private ImageButton playerOpenQueueButton; private ImageButton playerTrackInfo; + private LinearLayout ratingContainer; private MainActivity activity; private PlayerBottomSheetViewModel playerBottomSheetViewModel; @@ -123,6 +125,8 @@ public class PlayerControllerFragment extends Fragment { playerOpenQueueButton = bind.getRoot().findViewById(R.id.player_open_queue_button); playerTrackInfo = bind.getRoot().findViewById(R.id.player_info_track); songRatingBar = bind.getRoot().findViewById(R.id.song_rating_bar); + ratingContainer = bind.getRoot().findViewById(R.id.rating_container); + checkAndSetRatingContainerVisibility(); } private void initQuickActionView() { @@ -430,6 +434,17 @@ public class PlayerControllerFragment extends Fragment { playerMediaCoverViewPager.setCurrentItem(1, true); } + private void checkAndSetRatingContainerVisibility() { + if (ratingContainer == null) return; + + if (Preferences.showItemStarRating()) { + ratingContainer.setVisibility(View.VISIBLE); + } + else { + ratingContainer.setVisibility(View.GONE); + } + } + private void setPlaybackParameters(MediaBrowser mediaBrowser) { Button playbackSpeedButton = bind.getRoot().findViewById(R.id.player_playback_speed_button); float currentSpeed = Preferences.getPlaybackSpeed(); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerCoverFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerCoverFragment.java index 15f647c8..392c5786 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerCoverFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerCoverFragment.java @@ -9,6 +9,7 @@ import android.transition.TransitionManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import java.util.ArrayList; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; @@ -31,6 +32,7 @@ import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel; +import com.cappielloantonio.tempo.subsonic.models.Child; import com.google.android.material.snackbar.Snackbar; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -120,8 +122,10 @@ public class PlayerCoverFragment extends Fragment { }); bind.innerButtonTopRight.setOnClickListener(view -> { + ArrayList tracks = new ArrayList<>(); + tracks.add(song); Bundle bundle = new Bundle(); - bundle.putParcelable(Constants.TRACK_OBJECT, song); + bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, tracks); PlaylistChooserDialog dialog = new PlaylistChooserDialog(); dialog.setArguments(bundle); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java index 10f5d5a9..9250adfa 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java @@ -31,6 +31,7 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.dialog.DeleteDownloadStorageDialog; import com.cappielloantonio.tempo.ui.dialog.DownloadStorageDialog; import com.cappielloantonio.tempo.ui.dialog.StarredSyncDialog; +import com.cappielloantonio.tempo.ui.dialog.StarredAlbumSyncDialog; import com.cappielloantonio.tempo.ui.dialog.StreamingCacheStorageDialog; import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.Preferences; @@ -94,6 +95,7 @@ public class SettingsFragment extends PreferenceFragmentCompat { actionLogout(); actionScan(); + actionSyncStarredAlbums(); actionSyncStarredTracks(); actionChangeStreamingCacheStorage(); actionChangeDownloadStorage(); @@ -263,6 +265,18 @@ public class SettingsFragment extends PreferenceFragmentCompat { }); } + private void actionSyncStarredAlbums() { + findPreference("sync_starred_albums_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof Boolean) { + if ((Boolean) newValue) { + StarredAlbumSyncDialog dialog = new StarredAlbumSyncDialog(); + dialog.show(activity.getSupportFragmentManager(), null); + } + } + return true; + }); + } + private void actionChangeStreamingCacheStorage() { findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> { StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() { diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index cfcefe9d..f7b742e4 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -102,7 +102,7 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite); favoriteToggle.setChecked(albumBottomSheetViewModel.getAlbum().getStarred() != null); favoriteToggle.setOnClickListener(v -> { - albumBottomSheetViewModel.setFavorite(); + albumBottomSheetViewModel.setFavorite(requireContext()); }); TextView playRadio = view.findViewById(R.id.play_radio_text_view); diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt index e7a4c459..8c77ab13 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt @@ -37,6 +37,7 @@ object Preferences { private const val WIFI_ONLY = "wifi_only" private const val DATA_SAVING_MODE = "data_saving_mode" private const val SERVER_UNREACHABLE = "server_unreachable" + private const val SYNC_STARRED_ALBUMS_FOR_OFFLINE_USE = "sync_starred_albums_for_offline_use" private const val SYNC_STARRED_TRACKS_FOR_OFFLINE_USE = "sync_starred_tracks_for_offline_use" private const val QUEUE_SYNCING = "queue_syncing" private const val QUEUE_SYNCING_COUNTDOWN = "queue_syncing_countdown" @@ -63,6 +64,7 @@ object Preferences { private const val ALWAYS_ON_DISPLAY = "always_on_display" private const val AUDIO_QUALITY_PER_ITEM = "audio_quality_per_item" private const val HOME_SECTOR_LIST = "home_sector_list" + private const val SONG_RATING_PER_ITEM = "song_rating_per_item" private const val RATING_PER_ITEM = "rating_per_item" private const val NEXT_UPDATE_CHECK = "next_update_check" private const val CONTINUOUS_PLAY = "continuous_play" @@ -300,6 +302,18 @@ object Preferences { .apply() } + @JvmStatic + fun isStarredAlbumsSyncEnabled(): Boolean { + return App.getInstance().preferences.getBoolean(SYNC_STARRED_ALBUMS_FOR_OFFLINE_USE, false) + } + + @JvmStatic + fun setStarredAlbumsSyncEnabled(isStarredSyncEnabled: Boolean) { + App.getInstance().preferences.edit().putBoolean( + SYNC_STARRED_ALBUMS_FOR_OFFLINE_USE, isStarredSyncEnabled + ).apply() + } + @JvmStatic fun isStarredSyncEnabled(): Boolean { return App.getInstance().preferences.getBoolean(SYNC_STARRED_TRACKS_FOR_OFFLINE_USE, false) @@ -486,6 +500,11 @@ object Preferences { App.getInstance().preferences.edit().putString(HOME_SECTOR_LIST, Gson().toJson(extension)).apply() } + @JvmStatic + fun showItemStarRating(): Boolean { + return App.getInstance().preferences.getBoolean(SONG_RATING_PER_ITEM, false) + } + @JvmStatic fun showItemRating(): Boolean { return App.getInstance().preferences.getBoolean(RATING_PER_ITEM, false) diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumBottomSheetViewModel.java index afd0371a..da1ec831 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumBottomSheetViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumBottomSheetViewModel.java @@ -1,12 +1,15 @@ package com.cappielloantonio.tempo.viewmodel; import android.app.Application; +import android.content.Context; import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; +import com.cappielloantonio.tempo.model.Download; import com.cappielloantonio.tempo.interfaces.StarCallback; import com.cappielloantonio.tempo.repository.AlbumRepository; import com.cappielloantonio.tempo.repository.ArtistRepository; @@ -16,10 +19,14 @@ import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Share; +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 AlbumBottomSheetViewModel extends AndroidViewModel { private final AlbumRepository albumRepository; @@ -54,7 +61,7 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel { return albumRepository.getAlbumTracks(album.getId()); } - public void setFavorite() { + public void setFavorite(Context context) { if (album.getStarred() != null) { if (NetworkUtil.isOffline()) { removeFavoriteOffline(); @@ -65,7 +72,7 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel { if (NetworkUtil.isOffline()) { setFavoriteOffline(); } else { - setFavoriteOnline(); + setFavoriteOnline(context); } } } @@ -83,7 +90,6 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel { favoriteRepository.unstar(null, album.getId(), null, new StarCallback() { @Override public void onError() { - // album.setStarred(new Date()); favoriteRepository.starLater(null, album.getId(), null, false); } }); @@ -96,15 +102,31 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel { album.setStarred(new Date()); } - private void setFavoriteOnline() { + private void setFavoriteOnline(Context context) { favoriteRepository.star(null, album.getId(), null, new StarCallback() { @Override public void onError() { - // album.setStarred(null); favoriteRepository.starLater(null, album.getId(), null, true); } }); album.setStarred(new Date()); + if (Preferences.isStarredAlbumsSyncEnabled()) { + AlbumRepository albumRepository = new AlbumRepository(); + MutableLiveData> tracksLiveData = albumRepository.getAlbumTracks(album.getId()); + + tracksLiveData.observeForever(new Observer>() { + @Override + public void onChanged(List songs) { + if (songs != null && !songs.isEmpty()) { + DownloadUtil.getDownloadTracker(context).download( + MappingUtil.mapDownloads(songs), + songs.stream().map(Download::new).collect(Collectors.toList()) + ); + } + tracksLiveData.removeObserver(this); + } + }); + } } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java index b646bcf1..6477178c 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java @@ -47,6 +47,8 @@ public class HomeViewModel extends AndroidViewModel { private final PlaylistRepository playlistRepository; private final SharingRepository sharingRepository; + private final StarredAlbumsSyncViewModel albumsSyncViewModel; + private final MutableLiveData> dicoverSongSample = new MutableLiveData<>(null); private final MutableLiveData> newReleasedAlbum = new MutableLiveData<>(null); private final MutableLiveData> starredTracksSample = new MutableLiveData<>(null); @@ -82,6 +84,8 @@ public class HomeViewModel extends AndroidViewModel { playlistRepository = new PlaylistRepository(); sharingRepository = new SharingRepository(); + albumsSyncViewModel = new StarredAlbumsSyncViewModel(application); + setOfflineFavorite(); } @@ -166,6 +170,10 @@ public class HomeViewModel extends AndroidViewModel { return starredAlbums; } + public LiveData> getAllStarredAlbumSongs() { + return albumsSyncViewModel.getAllStarredAlbumSongs(); + } + public LiveData> getStarredArtists(LifecycleOwner owner) { if (starredArtists.getValue() == null) { artistRepository.getStarredArtists(true, 20).observe(owner, starredArtists::postValue); diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistChooserViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistChooserViewModel.java index fdee85c6..2ec6c21f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistChooserViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistChooserViewModel.java @@ -21,7 +21,7 @@ public class PlaylistChooserViewModel extends AndroidViewModel { private final PlaylistRepository playlistRepository; private final MutableLiveData> playlists = new MutableLiveData<>(null); - private ArrayList toAdd; + private ArrayList toAdd = new ArrayList<>(); public PlaylistChooserViewModel(@NonNull Application application) { super(application); diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/StarredAlbumsSyncViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/StarredAlbumsSyncViewModel.java new file mode 100644 index 00000000..5967caf8 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/StarredAlbumsSyncViewModel.java @@ -0,0 +1,90 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; +import android.app.Activity; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.repository.AlbumRepository; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.Child; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +public class StarredAlbumsSyncViewModel extends AndroidViewModel { + private final AlbumRepository albumRepository; + + private final MutableLiveData> starredAlbums = new MutableLiveData<>(null); + private final MutableLiveData> starredAlbumSongs = new MutableLiveData<>(null); + + public StarredAlbumsSyncViewModel(@NonNull Application application) { + super(application); + albumRepository = new AlbumRepository(); + } + + public LiveData> getStarredAlbums(LifecycleOwner owner) { + albumRepository.getStarredAlbums(false, -1).observe(owner, starredAlbums::postValue); + return starredAlbums; + } + + public LiveData> getAllStarredAlbumSongs() { + albumRepository.getStarredAlbums(false, -1).observeForever(new Observer>() { + @Override + public void onChanged(List albums) { + if (albums != null && !albums.isEmpty()) { + collectAllAlbumSongs(albums, starredAlbumSongs::postValue); + } else { + starredAlbumSongs.postValue(new ArrayList<>()); + } + albumRepository.getStarredAlbums(false, -1).removeObserver(this); + } + }); + + return starredAlbumSongs; + } + + public LiveData> getStarredAlbumSongs(Activity activity) { + albumRepository.getStarredAlbums(false, -1).observe((LifecycleOwner) activity, albums -> { + if (albums != null && !albums.isEmpty()) { + collectAllAlbumSongs(albums, starredAlbumSongs::postValue); + } else { + starredAlbumSongs.postValue(new ArrayList<>()); + } + }); + return starredAlbumSongs; + } + + private void collectAllAlbumSongs(List albums, AlbumSongsCallback callback) { + List allSongs = new ArrayList<>(); + CountDownLatch latch = new CountDownLatch(albums.size()); + + for (AlbumID3 album : albums) { + LiveData> albumTracks = albumRepository.getAlbumTracks(album.getId()); + albumTracks.observeForever(new Observer>() { + @Override + public void onChanged(List songs) { + if (songs != null) { + allSongs.addAll(songs); + } + latch.countDown(); + + if (latch.getCount() == 0) { + callback.onSongsCollected(allSongs); + albumTracks.removeObserver(this); + } + } + }); + } + } + + private interface AlbumSongsCallback { + void onSongsCollected(List songs); + } +} \ No newline at end of file diff --git a/app/src/main/res/layout-land/inner_fragment_player_controller_layout.xml b/app/src/main/res/layout-land/inner_fragment_player_controller_layout.xml index 61112689..7ad0250e 100644 --- a/app/src/main/res/layout-land/inner_fragment_player_controller_layout.xml +++ b/app/src/main/res/layout-land/inner_fragment_player_controller_layout.xml @@ -75,6 +75,39 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home_tab_music.xml b/app/src/main/res/layout/fragment_home_tab_music.xml index 06530614..e9811da3 100644 --- a/app/src/main/res/layout/fragment_home_tab_music.xml +++ b/app/src/main/res/layout/fragment_home_tab_music.xml @@ -106,6 +106,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/artist_list_menu.xml b/app/src/main/res/menu/artist_list_menu.xml new file mode 100644 index 00000000..7cc7d34d --- /dev/null +++ b/app/src/main/res/menu/artist_list_menu.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a41b43fa..8c6f2d99 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -90,6 +90,7 @@ Téléchargements Sélectionnez deux filtres ou plus Filtrer + Filtrer par artiste Filtrer par genre (%1$d) (+%1$d) @@ -162,6 +163,7 @@ Ajouter Ajouter à une playlist Télécharger tout + Noter l\'album Téléchargé Tout Téléchargé @@ -190,7 +192,11 @@ Année %1$.2fx Vider la file d\'attente + File d\'attente sauvegardée Priorité serveur + Format inconnu + Transcodage + demandé Catalogue des Playlists Parcourir les playlists Pas de playlist @@ -299,13 +305,15 @@ Débit binaire pour les téléchargements Débit binaire en données mobile Débit binaire en Wi-Fi - Taille du cache des fichiers audios + Taille du cache des fichiers audio Afficher les dossiers Si activé, rend possible la navigation dans les répertoires. À noter que pour que la navigation dans les dossiers fonctionne correctement, le serveur doit supporter cette fonctionnalité. Voir les podcasts - Si activé, rend visible la section Podcast + Si activé, rend visible la section Podcast. Redémarrez l\'application pour appliquer ce paramètre. Afficher la qualité audio Le débit binaire et le format audio seront affichés pour chaque piste. + Afficher la note de la piste + Si activé, rend visible la note de la piste sur sa page\n\n*Nécessite le redémarrage de l\'application Afficher la note Si activé, la note et le statut de mise en favori de l\'élément seront affichés. Minuteur de synchronisation diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9b6a76d1..2ffe2c60 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -90,6 +90,7 @@ Downloads Select two or more filters Filter + Filter artists Filter Genres (%1$d) (+%1$d) @@ -116,6 +117,8 @@ Download Downloading these tracks may involve significant data usage Looks like there are some starred tracks to sync + Sync Starred Albums + Albums marked with a star will be available offline Best of Discovery Shuffle all @@ -164,6 +167,7 @@ Add Add to playlist Download all + Rate album Download All Downloaded @@ -312,6 +316,8 @@ If enabled, show the podcast section. Restart the app for it to take full effect. Show audio quality The bitrate and audio format will be shown for each audio track. + Show song star rating + If enabled, hides 5 star rating for track on song page\n\n*Requires App restart Show item rating If enabled, the item\'s rating and whether it is marked as a favorite will be displayed. Sync timer @@ -340,6 +346,8 @@ Priority given to the transcoding mode. If set to \"Direct play\" the bitrate of the file will not be changed. Download transcoded media. If enabled, the download endpoint will not be used, but the following settings. \n\n If \"Transcode format for donwloads\" is set to \"Direct download\" the bitrate of the file will not be changed. When the file is transcoded on the fly, the client usually does not show the track length. It is possible to request the servers that support the functionality to estimate the duration of the track being played, but the response times may take longer. + If enabled, starred albums will be downloaded for offline use. + Sync starred albums for offline use If enabled, starred tracks will be downloaded for offline use. Sync starred tracks for offline use Theme @@ -393,8 +401,10 @@ Cancel Continue Continue and download - Downloading starry tracks may require a large amount of data. + Downloading starred tracks may require a large amount of data. Sync starred tracks + Downloading starred albums may require a large amount of data. + Sync starred albums For the changes to take effect, restart the app. Changing the destination of cached files from one storage to another may result in the deletion of any previously cached files in the other storage. Select storage option @@ -429,4 +439,8 @@ unDraw A special thanks goes to unDraw without whose illustrations we could not have made this application more beautiful. https://undraw.co/ + + %d album to sync + %d albums to sync + diff --git a/app/src/main/res/xml/global_preferences.xml b/app/src/main/res/xml/global_preferences.xml index c90779b2..09afcb1d 100644 --- a/app/src/main/res/xml/global_preferences.xml +++ b/app/src/main/res/xml/global_preferences.xml @@ -57,6 +57,12 @@ android:summary="@string/settings_audio_quality_summary" android:key="audio_quality_per_item" /> + + + +