From f854f49686d7a917b8e083b8c7c246a94fd2c05f Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 30 Aug 2025 09:04:25 -0700 Subject: [PATCH] feat: adds sync starred albums functionality #66 --- .../ui/dialog/StarredAlbumSyncDialog.java | 81 +++++++++++++++++++ .../tempo/ui/fragment/SettingsFragment.java | 14 ++++ .../AlbumBottomSheetDialog.java | 2 +- .../tempo/util/Preferences.kt | 13 +++ .../viewmodel/AlbumBottomSheetViewModel.java | 32 ++++++-- .../viewmodel/StarredAlbumsSyncViewModel.java | 70 ++++++++++++++++ .../res/layout/dialog_starred_album_sync.xml | 14 ++++ app/src/main/res/values/strings.xml | 6 +- app/src/main/res/xml/global_preferences.xml | 6 ++ 9 files changed, 231 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/cappielloantonio/tempo/ui/dialog/StarredAlbumSyncDialog.java create mode 100644 app/src/main/java/com/cappielloantonio/tempo/viewmodel/StarredAlbumsSyncViewModel.java create mode 100644 app/src/main/res/layout/dialog_starred_album_sync.xml 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/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 c7bfe993..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" @@ -301,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) 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/StarredAlbumsSyncViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/StarredAlbumsSyncViewModel.java new file mode 100644 index 00000000..0f45bedd --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/StarredAlbumsSyncViewModel.java @@ -0,0 +1,70 @@ +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.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> 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) { + albumRepository.getAlbumTracks(album.getId()).observeForever(songs -> { + if (songs != null) { + allSongs.addAll(songs); + } + latch.countDown(); + + if (latch.getCount() == 0) { + callback.onSongsCollected(allSongs); + } + }); + } + + albumRepository.removeObserver(this); + } + + private interface AlbumSongsCallback { + void onSongsCollected(List songs); + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_starred_album_sync.xml b/app/src/main/res/layout/dialog_starred_album_sync.xml new file mode 100644 index 00000000..5b9343dc --- /dev/null +++ b/app/src/main/res/layout/dialog_starred_album_sync.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ef389aeb..e721bcd9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -344,6 +344,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 @@ -397,8 +399,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 diff --git a/app/src/main/res/xml/global_preferences.xml b/app/src/main/res/xml/global_preferences.xml index ddbcc1a7..09afcb1d 100644 --- a/app/src/main/res/xml/global_preferences.xml +++ b/app/src/main/res/xml/global_preferences.xml @@ -139,6 +139,12 @@ android:summary="@string/settings_sync_starred_tracks_for_offline_use_summary" android:key="sync_starred_tracks_for_offline_use" /> + +