feat: adds sync starred albums functionality #66 (#73)

This commit is contained in:
eddyizm 2025-08-30 09:06:47 -07:00 committed by GitHub
commit 31d91f7215
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 231 additions and 7 deletions

View file

@ -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();
});
}
}
}

View file

@ -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() {

View file

@ -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);

View file

@ -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)

View file

@ -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<List<Child>> tracksLiveData = albumRepository.getAlbumTracks(album.getId());
tracksLiveData.observeForever(new Observer<List<Child>>() {
@Override
public void onChanged(List<Child> songs) {
if (songs != null && !songs.isEmpty()) {
DownloadUtil.getDownloadTracker(context).download(
MappingUtil.mapDownloads(songs),
songs.stream().map(Download::new).collect(Collectors.toList())
);
}
tracksLiveData.removeObserver(this);
}
});
}
}
}

View file

@ -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<List<AlbumID3>> starredAlbums = new MutableLiveData<>(null);
private final MutableLiveData<List<Child>> starredAlbumSongs = new MutableLiveData<>(null);
public StarredAlbumsSyncViewModel(@NonNull Application application) {
super(application);
albumRepository = new AlbumRepository();
}
public LiveData<List<AlbumID3>> getStarredAlbums(LifecycleOwner owner) {
albumRepository.getStarredAlbums(false, -1).observe(owner, starredAlbums::postValue);
return starredAlbums;
}
public LiveData<List<Child>> 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<AlbumID3> albums, AlbumSongsCallback callback) {
List<Child> 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<Child> songs);
}
}

View file

@ -0,0 +1,14 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="4dp"
android:text="@string/starred_album_sync_dialog_summary" />
</LinearLayout>

View file

@ -344,6 +344,8 @@
<string name="settings_summary_transcoding">Priority given to the transcoding mode. If set to \"Direct play\" the bitrate of the file will not be changed.</string>
<string name="settings_summary_transcoding_download">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.</string>
<string name="settings_summary_transcoding_estimate_content_length">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.</string>
<string name="settings_sync_starred_albums_for_offline_use_summary">If enabled, starred albums will be downloaded for offline use.</string>
<string name="settings_sync_starred_albums_for_offline_use_title">Sync starred albums for offline use</string>
<string name="settings_sync_starred_tracks_for_offline_use_summary">If enabled, starred tracks will be downloaded for offline use.</string>
<string name="settings_sync_starred_tracks_for_offline_use_title">Sync starred tracks for offline use</string>
<string name="settings_theme">Theme</string>
@ -397,8 +399,10 @@
<string name="starred_sync_dialog_negative_button">Cancel</string>
<string name="starred_sync_dialog_neutral_button">Continue</string>
<string name="starred_sync_dialog_positive_button">Continue and download</string>
<string name="starred_sync_dialog_summary">Downloading starry tracks may require a large amount of data.</string>
<string name="starred_sync_dialog_summary">Downloading starred tracks may require a large amount of data.</string>
<string name="starred_sync_dialog_title">Sync starred tracks</string>
<string name="starred_album_sync_dialog_summary">Downloading starred albums may require a large amount of data.</string>
<string name="starred_album_sync_dialog_title">Sync starred albums</string>
<string name="streaming_cache_storage_dialog_sub_summary">For the changes to take effect, restart the app.</string>
<string name="streaming_cache_storage_dialog_summary">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.</string>
<string name="streaming_cache_storage_dialog_title">Select storage option</string>

View file

@ -139,6 +139,12 @@
android:summary="@string/settings_sync_starred_tracks_for_offline_use_summary"
android:key="sync_starred_tracks_for_offline_use" />
<SwitchPreference
android:title="@string/settings_sync_starred_albums_for_offline_use_title"
android:defaultValue="false"
android:summary="@string/settings_sync_starred_albums_for_offline_use_summary"
android:key="sync_starred_albums_for_offline_use" />
<ListPreference
app:defaultValue="1"
app:dialogTitle="@string/settings_buffering_strategy"