diff --git a/.gitignore b/.gitignore index 74483036..0f48176e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ .cxx /.idea/ .env +.vscode/settings.json \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 1d411cd3..d5950937 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -192,6 +192,7 @@ + diff --git a/README.md b/README.md index 84b4b864..79b2081d 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,12 @@

- +

-

+ **Tempo** is an open-source and lightweight music client for Subsonic, designed and built natively for Android. It provides a seamless and intuitive music streaming experience, allowing you to access and play your Subsonic music library directly from your Android device. @@ -22,6 +22,21 @@ Tempo does not rely on magic algorithms to decide what you should listen to. Ins **Use the Github version of the app for full Android Auto and Chromecast support.** +## Fork + +This fork is my attempt to keep development moving forward and merge in PR's that have been sitting for a while in the main repo. Thankful to @CappielloAntonio for the amazing app and hopefully we can continue to build on top of it. I will only be releasing on github and if I am not able to merge back to the main repo, I plan to rename the app to be able to publish it to fdroid and possibly google play? We will see. + +v3.10.0 applies the following PR's (fix/feat/chore): +fix: [379](https://github.com/CappielloAntonio/tempo/pull/379) -Fix: redirection to artist fragment on artist label click +fix: [385](https://github.com/CappielloAntonio/tempo/pull/385) -Player queue lag, limits +fix: [389](https://github.com/CappielloAntonio/tempo/pull/389) -Fix crash when sorting albums with a null artist +feat: [371](https://github.com/CappielloAntonio/tempo/pull/371) -Display toast message after adding a song to a playlist +feat: [367](https://github.com/CappielloAntonio/tempo/pull/367) -Album add to playlist context menu item +chore: [374](https://github.com/CappielloAntonio/tempo/pull/374) -Spanish translation +feat: [397](https://github.com/CappielloAntonio/tempo/pull/397) -Store and retrieve replay and shuffle states in preferences +feat:[400](https://github.com/CappielloAntonio/tempo/pull/400) - enhance Android media player notification window +chore: [378](https://github.com/CappielloAntonio/tempo/pull/378) Polish translation + ## Features - **Subsonic Integration**: Tempo seamlessly integrates with your Subsonic server, providing you with easy access to your entire music collection on the go. - **Sleek and Intuitive UI**: Enjoy a clean and user-friendly interface designed to enhance your music listening experience, tailored to your preferences and listening history. @@ -42,7 +57,7 @@ Tempo does not rely on magic algorithms to decide what you should listen to. Ins - +

diff --git a/app/build.gradle b/app/build.gradle index b6ee7b49..17540307 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { targetSdk 35 versionCode 26 - versionName '3.9.0' + versionName '3.10.0-alpha.1' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' @@ -107,4 +107,9 @@ dependencies { implementation 'com.squareup.retrofit2:retrofit:2.11.0' implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14' implementation 'com.squareup.retrofit2:converter-gson:2.11.0' +} +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 986481bd..cc9990e7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,6 @@ - diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/PlaylistRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/PlaylistRepository.java index bcf0c732..7884159f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/PlaylistRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/PlaylistRepository.java @@ -1,10 +1,16 @@ package com.cappielloantonio.tempo.repository; +import static android.provider.Settings.System.getString; + +import android.provider.Settings; +import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.database.AppDatabase; import com.cappielloantonio.tempo.database.dao.PlaylistDao; import com.cappielloantonio.tempo.subsonic.base.ApiResponse; @@ -20,6 +26,7 @@ import retrofit2.Callback; import retrofit2.Response; public class PlaylistRepository { + @androidx.media3.common.util.UnstableApi private final PlaylistDao playlistDao = AppDatabase.getInstance().playlistDao(); public MutableLiveData> getPlaylists(boolean random, int size) { MutableLiveData> listLivePlaylists = new MutableLiveData<>(new ArrayList<>()); @@ -80,12 +87,12 @@ public class PlaylistRepository { .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { - + Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_add_success), Toast.LENGTH_SHORT).show(); } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - + Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_add_failure), Toast.LENGTH_SHORT).show(); } }); } @@ -157,17 +164,19 @@ public class PlaylistRepository { } }); } - + @androidx.media3.common.util.UnstableApi public LiveData> getPinnedPlaylists() { return playlistDao.getAll(); } + @androidx.media3.common.util.UnstableApi public void insert(Playlist playlist) { InsertThreadSafe insert = new InsertThreadSafe(playlistDao, playlist); Thread thread = new Thread(insert); thread.start(); } + @androidx.media3.common.util.UnstableApi public void delete(Playlist playlist) { DeleteThreadSafe delete = new DeleteThreadSafe(playlistDao, playlist); Thread thread = new Thread(delete); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java index 1fc970ab..1f360c80 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java @@ -157,7 +157,7 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter thumbnail = CustomGlideRequest.Builder + .from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song) + .build() + .sizeMultiplier(0.1f); + CustomGlideRequest.Builder .from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song) .build() + .thumbnail(thumbnail) .into(holder.item.queueSongCoverImageView); MediaManager.getCurrentIndex(mediaBrowserListenableFuture, new MediaIndexCallback() { @Override public void onRecovery(int index) { - if (position < index) { + if (holder.getLayoutPosition() < index) { holder.item.queueSongTitleTextView.setAlpha(0.2f); holder.item.queueSongSubtitleTextView.setAlpha(0.2f); holder.item.ratingIndicatorImageView.setAlpha(0.2f); 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 f0266c84..5c0b119c 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 @@ -57,14 +57,14 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba } private void setSongInfo() { - playlistChooserViewModel.setSongToAdd(requireArguments().getParcelable(Constants.TRACK_OBJECT)); + playlistChooserViewModel.setSongsToAdd(requireArguments().getParcelableArrayList(Constants.TRACKS_OBJECT)); } private void setButtonAction() { androidx.appcompat.app.AlertDialog alertDialog = (androidx.appcompat.app.AlertDialog) Objects.requireNonNull(getDialog()); alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> { Bundle bundle = new Bundle(); - bundle.putParcelable(Constants.TRACK_OBJECT, playlistChooserViewModel.getSongToAdd()); + bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, playlistChooserViewModel.getSongsToAdd()); PlaylistEditorDialog dialog = new PlaylistEditorDialog(null); dialog.setArguments(bundle); @@ -98,7 +98,7 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba @Override public void onPlaylistClick(Bundle bundle) { Playlist playlist = bundle.getParcelable(Constants.PLAYLIST_OBJECT); - playlistChooserViewModel.addSongToPlaylist(playlist.getId()); + playlistChooserViewModel.addSongsToPlaylist(playlist.getId()); dismiss(); } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistEditorDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistEditorDialog.java index 4d265beb..dea70d7d 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistEditorDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistEditorDialog.java @@ -74,11 +74,11 @@ public class PlaylistEditorDialog extends DialogFragment { } private void setParameterInfo() { - if (requireArguments().getParcelable(Constants.TRACK_OBJECT) != null) { - playlistEditorViewModel.setSongToAdd(requireArguments().getParcelable(Constants.TRACK_OBJECT)); + if (requireArguments().getParcelableArrayList(Constants.TRACKS_OBJECT) != null) { + playlistEditorViewModel.setSongsToAdd(requireArguments().getParcelableArrayList(Constants.TRACKS_OBJECT)); playlistEditorViewModel.setPlaylistToEdit(null); } else if (requireArguments().getParcelable(Constants.PLAYLIST_OBJECT) != null) { - playlistEditorViewModel.setSongToAdd(null); + playlistEditorViewModel.setSongsToAdd(null); playlistEditorViewModel.setPlaylistToEdit(requireArguments().getParcelable(Constants.PLAYLIST_OBJECT)); if (playlistEditorViewModel.getPlaylistToEdit() != null) { @@ -92,7 +92,7 @@ public class PlaylistEditorDialog extends DialogFragment { alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> { if (validateInput()) { - if (playlistEditorViewModel.getSongToAdd() != null) { + if (playlistEditorViewModel.getSongsToAdd() != null) { playlistEditorViewModel.createPlaylist(playlistName); } else if (playlistEditorViewModel.getPlaylistToEdit() != null) { playlistEditorViewModel.updatePlaylist(playlistName); 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 8162b16b..f830888e 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 @@ -31,6 +31,7 @@ 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.util.Constants; import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.MappingUtil; @@ -38,6 +39,7 @@ import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.viewmodel.AlbumPageViewModel; import com.google.common.util.concurrent.ListenableFuture; +import java.util.ArrayList; import java.util.Collections; import java.util.Objects; import java.util.stream.Collectors; @@ -108,6 +110,17 @@ public class AlbumPageFragment extends Fragment implements ClickCallback { }); return true; } + if (item.getItemId() == R.id.action_add_to_playlist) { + albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> { + Bundle bundle = new Bundle(); + bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(songs)); + + PlaylistChooserDialog dialog = new PlaylistChooserDialog(); + dialog.setArguments(bundle); + dialog.show(requireActivity().getSupportFragmentManager(), null); + }); + return true; + } return false; } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java index dafd1507..b3755ee3 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java @@ -112,6 +112,9 @@ public class PlayerBottomSheetFragment extends Fragment { try { MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get(); + mediaBrowser.setShuffleModeEnabled(Preferences.isShuffleModeEnabled()); + mediaBrowser.setRepeatMode(Preferences.getRepeatMode()); + setMediaControllerListener(mediaBrowser); } catch (Exception e) { e.printStackTrace(); @@ -150,6 +153,16 @@ public class PlayerBottomSheetFragment extends Fragment { public void onEvents(Player player, Player.Events events) { setHeaderNextButtonState(mediaBrowser.hasNextMediaItem()); } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + Preferences.setShuffleModeEnabled(shuffleModeEnabled); + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + Preferences.setRepeatMode(repeatMode); + } }); } 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 1d474db9..d1daae9b 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 @@ -140,6 +140,8 @@ public class PlayerControllerFragment extends Fragment { MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get(); bind.nowPlayingMediaControllerView.setPlayer(mediaBrowser); + mediaBrowser.setShuffleModeEnabled(Preferences.isShuffleModeEnabled()); + mediaBrowser.setRepeatMode(Preferences.getRepeatMode()); setMediaControllerListener(mediaBrowser); } catch (Exception e) { @@ -160,6 +162,16 @@ public class PlayerControllerFragment extends Fragment { setMetadata(mediaMetadata); setMediaInfo(mediaMetadata); } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + Preferences.setShuffleModeEnabled(shuffleModeEnabled); + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + Preferences.setRepeatMode(repeatMode); + } }); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java index 39c72468..55b46ff2 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java @@ -193,13 +193,13 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback { playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> { if (bind != null) { bind.playlistPagePlayButton.setOnClickListener(v -> { - MediaManager.startQueue(mediaBrowserListenableFuture, songs.subList(0, Math.min(100, songs.size())), 0); + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); activity.setBottomSheetInPeek(true); }); bind.playlistPageShuffleButton.setOnClickListener(v -> { Collections.shuffle(songs); - MediaManager.startQueue(mediaBrowserListenableFuture, songs.subList(0, Math.min(100, songs.size())), 0); + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); activity.setBottomSheetInPeek(true); }); } @@ -270,4 +270,4 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback { public void onMediaLongClick(Bundle bundle) { Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); } -} \ No newline at end of file +} 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 a498f473..cfcefe9d 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 @@ -31,6 +31,7 @@ import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog; import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.MappingUtil; @@ -167,6 +168,20 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements }); }); + TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view); + addToPlaylist.setOnClickListener(v -> { + albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> { + Bundle bundle = new Bundle(); + bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(songs)); + + PlaylistChooserDialog dialog = new PlaylistChooserDialog(); + dialog.setArguments(bundle); + dialog.show(requireActivity().getSupportFragmentManager(), null); + + dismissBottomSheet(); + }); + }); + TextView removeAll = view.findViewById(R.id.remove_all_text_view); albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> { List mediaItems = MappingUtil.mapDownloads(songs); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java index b22830eb..0d15ad2f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java @@ -39,6 +39,9 @@ import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.google.common.util.concurrent.ListenableFuture; +import java.util.ArrayList; +import java.util.Collections; + @UnstableApi public class SongBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener { private HomeViewModel homeViewModel; @@ -177,7 +180,7 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view); addToPlaylist.setOnClickListener(v -> { Bundle bundle = new Bundle(); - bundle.putParcelable(Constants.TRACK_OBJECT, song); + bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(Collections.singletonList(song))); PlaylistChooserDialog dialog = new PlaylistChooserDialog(); dialog.setArguments(bundle); 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 5cbe9035..e7a4c459 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt @@ -1,6 +1,7 @@ package com.cappielloantonio.tempo.util import android.util.Log +import androidx.media3.common.Player import com.cappielloantonio.tempo.App import com.cappielloantonio.tempo.model.HomeSector import com.cappielloantonio.tempo.subsonic.models.OpenSubsonicExtension @@ -24,6 +25,8 @@ object Preferences { private const val NEXT_SERVER_SWITCH = "next_server_switch" private const val PLAYBACK_SPEED = "playback_speed" private const val SKIP_SILENCE = "skip_silence" + private const val SHUFFLE_MODE = "shuffle_mode" + private const val REPEAT_MODE = "repeat_mode" private const val IMAGE_CACHE_SIZE = "image_cache_size" private const val STREAMING_CACHE_SIZE = "streaming_cache_size" private const val IMAGE_SIZE = "image_size" @@ -226,6 +229,26 @@ object Preferences { App.getInstance().preferences.edit().putBoolean(SKIP_SILENCE, isSkipSilenceMode).apply() } + @JvmStatic + fun isShuffleModeEnabled(): Boolean { + return App.getInstance().preferences.getBoolean(SHUFFLE_MODE, false) + } + + @JvmStatic + fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) { + App.getInstance().preferences.edit().putBoolean(SHUFFLE_MODE, shuffleModeEnabled).apply() + } + + @JvmStatic + fun getRepeatMode(): Int { + return App.getInstance().preferences.getInt(REPEAT_MODE, Player.REPEAT_MODE_OFF) + } + + @JvmStatic + fun setRepeatMode(repeatMode: Int) { + App.getInstance().preferences.edit().putInt(REPEAT_MODE, repeatMode).apply() + } + @JvmStatic fun getImageCacheSize(): Int { return App.getInstance().preferences.getString(IMAGE_CACHE_SIZE, "500")!!.toInt() 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 6c244505..0979f408 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumPageViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumPageViewModel.java @@ -21,6 +21,7 @@ public class AlbumPageViewModel extends AndroidViewModel { private final AlbumRepository albumRepository; private final ArtistRepository artistRepository; private String albumId; + private String artistId; private final MutableLiveData album = new MutableLiveData<>(null); public AlbumPageViewModel(@NonNull Application application) { @@ -41,6 +42,7 @@ public class AlbumPageViewModel extends AndroidViewModel { public void setAlbum(LifecycleOwner owner, AlbumID3 album) { this.albumId = album.getId(); this.album.postValue(album); + this.artistId = album.getArtistId(); albumRepository.getAlbum(album.getId()).observe(owner, albums -> { if (albums != null) this.album.setValue(albums); @@ -48,7 +50,7 @@ public class AlbumPageViewModel extends AndroidViewModel { } public LiveData getArtist() { - return artistRepository.getArtistInfo(albumId); + return artistRepository.getArtistInfo(artistId); } public LiveData getAlbumInfo() { 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 a780d1cf..b646bcf1 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java @@ -94,7 +94,7 @@ public class HomeViewModel extends AndroidViewModel { } public LiveData> getRandomShuffleSample() { - return songRepository.getRandomSample(100, null, null); + return songRepository.getRandomSample(1000, null, null); } public LiveData> getChronologySample(LifecycleOwner owner) { 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 cb8833fe..fdee85c6 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistChooserViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistChooserViewModel.java @@ -11,6 +11,7 @@ import androidx.lifecycle.MutableLiveData; import com.cappielloantonio.tempo.repository.PlaylistRepository; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Playlist; +import com.google.common.collect.Lists; import java.util.ArrayList; import java.util.Collections; @@ -20,7 +21,7 @@ public class PlaylistChooserViewModel extends AndroidViewModel { private final PlaylistRepository playlistRepository; private final MutableLiveData> playlists = new MutableLiveData<>(null); - private Child toAdd; + private ArrayList toAdd; public PlaylistChooserViewModel(@NonNull Application application) { super(application); @@ -33,15 +34,15 @@ public class PlaylistChooserViewModel extends AndroidViewModel { return playlists; } - public void addSongToPlaylist(String playlistId) { - playlistRepository.addSongToPlaylist(playlistId, new ArrayList(Collections.singletonList(toAdd.getId()))); + public void addSongsToPlaylist(String playlistId) { + playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(Lists.transform(toAdd, Child::getId))); } - public void setSongToAdd(Child song) { - toAdd = song; + public void setSongsToAdd(ArrayList songs) { + toAdd = songs; } - public Child getSongToAdd() { + public ArrayList getSongsToAdd() { return toAdd; } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistEditorViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistEditorViewModel.java index 8f9181a0..aceedb95 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistEditorViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistEditorViewModel.java @@ -12,6 +12,7 @@ import com.cappielloantonio.tempo.repository.SharingRepository; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Playlist; import com.cappielloantonio.tempo.subsonic.models.Share; +import com.google.common.collect.Lists; import java.util.ArrayList; import java.util.Collections; @@ -24,7 +25,7 @@ public class PlaylistEditorViewModel extends AndroidViewModel { private final PlaylistRepository playlistRepository; private final SharingRepository sharingRepository; - private Child toAdd; + private ArrayList toAdd; private Playlist toEdit; private MutableLiveData> songLiveList = new MutableLiveData<>(); @@ -37,7 +38,7 @@ public class PlaylistEditorViewModel extends AndroidViewModel { } public void createPlaylist(String name) { - playlistRepository.createPlaylist(null, name, new ArrayList(Collections.singletonList(toAdd.getId()))); + playlistRepository.createPlaylist(null, name, new ArrayList(Lists.transform(toAdd, Child::getId))); } public void updatePlaylist(String name) { @@ -48,12 +49,12 @@ public class PlaylistEditorViewModel extends AndroidViewModel { if (toEdit != null) playlistRepository.deletePlaylist(toEdit.getId()); } - public Child getSongToAdd() { - return toAdd; + public void setSongsToAdd(ArrayList songs) { + toAdd = songs; } - public void setSongToAdd(Child song) { - this.toAdd = song; + public ArrayList getSongsToAdd() { + return toAdd; } public Playlist getPlaylistToEdit() { diff --git a/app/src/main/res/layout/bottom_sheet_album_dialog.xml b/app/src/main/res/layout/bottom_sheet_album_dialog.xml index a6ed37a5..b37a5f90 100644 --- a/app/src/main/res/layout/bottom_sheet_album_dialog.xml +++ b/app/src/main/res/layout/bottom_sheet_album_dialog.xml @@ -146,6 +146,19 @@ android:paddingBottom="12dp" android:text="@string/album_bottom_sheet_download_all" /> + + + + - + android:layout_height="match_parent" + android:layout_marginTop="40dp" + android:paddingTop="8dp" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> - - - - - - - - + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5c627b9a..1df280e0 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -3,6 +3,7 @@ Bitte deaktiviere die Batterieoptimierung, damit die Medienwiedergabe bei ausgeschaltetem Bildschirm richtig funktioniert. Batterie Optimierung Offlinebetrieb + Zu Playliste hinzufügen Zur Warteschlange hinzufügen Alle herunterladen Gehe zu Künstler @@ -154,6 +155,7 @@ Subsonic Server Cast Hinzufügen + Zu Playliste hinzufügen Alle Herunterladen Downloads Alle @@ -186,6 +188,8 @@ Abbrechen Erstellen Zu einer Playliste hinzufügen + Lied zu Playlist hinzugefügt + Titel kann nicht zur Playlist hinzugefügt werden %1$d Tracks • %2$s Länge • %1$s Zum Löschen lange drücken @@ -277,7 +281,7 @@ Download storage Audio Einstellungen anpassen Equalizer - https://github.com/CappielloAntonio/tempo + https://github.com/eddyizm/tempo Verfolge die Entwicklung Github Bilder Auflösung anpassen diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 693e3661..d2c21d36 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -165,7 +165,7 @@ Resolución de la imagen Idioma Cerrar sesión - https://github.com/CappielloAntonio/tempo + https://github.com/eddyizm/tempo Siga el desarrollo Github Género diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 03a2f0db..aebaee19 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -3,6 +3,7 @@ Veuillez désactiver les optimisations de la batterie pour permettre la lecture des médias lorsque l\'écran est éteint. Optimisations de la batterie Mode hors-ligne + Ajouter à une playlist Ajouter à la file d\'attente Télécharger tout Aller à l\'artiste @@ -133,6 +134,7 @@ Serveurs Subsonic Cast Ajouter + Ajouter à une playlist Télécharger tout Téléchargé Tout @@ -244,7 +246,7 @@ Stockage des téléchargements Ajuster les paramètres audios Égaliseur - https://github.com/CappielloAntonio/tempo + https://github.com/eddyizm/tempo Suivre le développement Github Définir la résolution des images diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 2cc5bcb6..a5f3be74 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -3,7 +3,8 @@ Per favore, disabilita le ottimizzazioni della batteria per la riproduzione multimediale quando lo schermo è spento. Ottimizzazioni della Batteria Modalità offline - Aggiungi alla coda + Aggiungi alla playlist + Aggiungi alla coda Scarica tutto Vai all\'artista Mix istantaneo @@ -156,7 +157,8 @@ Server Subsonic Trasmetti Aggiungi - Scarica tutto + Aggiungi alla playlist + Scarica tutto Scarica Tutti Scaricati @@ -188,6 +190,8 @@ Annulla Crea Aggiungi a una playlist + Aggiunta di un brano alla playlist + Impossibile aggiungere un brano alla playlist %1$d brani • %2$s Durata • %1$s Premi a lungo per eliminare @@ -279,7 +283,7 @@ Archivio download Regola le impostazioni audio Equalizzatore - https://github.com/CappielloAntonio/tempo + https://github.com/eddyizm/tempo Segui lo sviluppo Github Imposta risoluzione delle immagini diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 679f333a..1953612f 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -3,6 +3,7 @@ 화면이 꺼진 상태에서 음악 재생을 하기 위해서는 배터리 최적화를 비활성화 해주세요. 배터리 최적화 오프라인 모드 + 플레이리스트에 추가 재생목록에 추가 모두 다운로드 아티스트로 이동 @@ -135,6 +136,7 @@ Subsonic 서버 Cast 추가 + 플레이리스트에 추가 모두 다운로드 다운로드 모두 @@ -161,6 +163,8 @@ 취소 생성 플레이리스트 추가 + 재생 목록에 노래 추가 + 재생 목록에 노래를 추가하지 못했습니다. %1$d 트랙 • %2$s 재생시간 • %1$s 플레이리스트 이름 @@ -246,7 +250,7 @@ 스토리지 다운로드 오디오 설정 적용 이퀄라이저 - https://github.com/CappielloAntonio/tempo + https://github.com/eddyizm/tempo Follow the development Github 이미지 해상도 설정 diff --git a/app/src/main/res/values-pl/arrays.xml b/app/src/main/res/values-pl/arrays.xml new file mode 100644 index 00000000..f3105914 --- /dev/null +++ b/app/src/main/res/values-pl/arrays.xml @@ -0,0 +1,257 @@ + + + Jasny + Ciemny + Domyślny systemu + + + jasny + ciemny + domyślny + + + + Duży + Średni + Mały + + + 500 + 250 + 125 + + + + Duża + Średnia + Mała + + + -1 + 500 + 300 + + + + Wyłączone + 128 MiB + 256 MiB + 512 MiB + 1024 MiB + + + 0 + 128 + 256 + 512 + 1024 + + + + Oryginalny + 32 kbps + 48 kbps + 64 kbps + 80 kbps + 96 kbps + 112 kbps + 128 kbps + 160 kbps + 192 kbps + 256 kbps + 320 kbps + + + 0 + 32 + 48 + 64 + 80 + 96 + 112 + 128 + 160 + 192 + 256 + 320 + + + + Oryginalny + 32 kbps + 48 kbps + 64 kbps + 80 kbps + 96 kbps + 112 kbps + 128 kbps + 160 kbps + 192 kbps + 256 kbps + 320 kbps + + + 0 + 32 + 48 + 64 + 80 + 96 + 112 + 128 + 160 + 192 + 256 + 320 + + + + Oryginalny + 32 kbps + 48 kbps + 64 kbps + 80 kbps + 96 kbps + 112 kbps + 128 kbps + 160 kbps + 192 kbps + 256 kbps + 320 kbps + + + 0 + 32 + 48 + 64 + 80 + 96 + 112 + 128 + 160 + 192 + 256 + 320 + + + + Odtwarzanie bezpośrednie + Opus + AAC + Mp3 + Flac + + + raw + opus + aac + mp3 + flac + + + + Odtwarzanie bezpośrednie + Opus + AAC + Mp3 + Flac + + + raw + opus + aac + mp3 + flac + + + + Pobieranie bezpośrednie + Opus + AAC + Mp3 + Flac + + + raw + opus + aac + mp3 + flac + + + + Dziesięć sekund + Pięć sekund + Dwie sekundy + + + 10 + 5 + 2 + + + + Duży + Średni + Mały + + + 18 + 12 + 6 + + + + Wyłączony + Utwór + Album + Auto + + + wyłączony + utwór + album + auto + + + + Nie transkoduj + Ustawienia serwera + Format transkodowania Wi-FI + Format transkodowania sieci komórkowej + + + 0 + 1 + 2 + 3 + + + + Minimalna + Średnia + Agresywna + Ekstremalna + + + .1 + 1 + 4 + 8 + + + + Minimum 0 gwiazdek + Minimum 1 gwiazdka + Minimum 2 gwiazdki + Minimum 3 gwiazdki + Minimum 4 gwiazdki + + + 0 + 1 + 2 + 3 + 4 + + \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml new file mode 100644 index 00000000..37f1685a --- /dev/null +++ b/app/src/main/res/values-pl/strings.xml @@ -0,0 +1,418 @@ + + Jeżeli masz problemy odwiedź stronę https://dontkillmyapp.com. Podaje ona dokładne instrukcje na temat tego jak wyłączyć funkcje oszczędzania energii które mogą wpływać na wydajność aplikacji. + Wyłącz optymalizacje baterii aby odtwarzać media przy wyłączonym ekranie. + Optymalizcje Baterii + Tryb offline + Dodaj do kolejki + Pobierz wszystkie + Przejdź do wykonawcy + Natychmiastowy mix + Odtwórz jako następne + Usuń wszystkie + Udostępnij + Odtwórz losowo + Albumy + Przeglądaj Albumy + Błąd podczas pobierania wykonawcy + Pobrane albumy + Najczęściej odtwarzane albumy + Nowe wydania + Albumy dodane niedawno + Albumy odtwarzane niedawno + Albumy oznaczone gwiazdką + Albumy + Więcej podobnych + Odtwarzaj + Wydane %1$s + Wydane %1$s, oryginalnie %2$s + Odtwarzaj losowo + %1$d utworów • %2$d minut + Tempo + Szukanie… + Natychmiastowy mix mix + Odtwórz losowo + Wykonawcy + Przeglądaj wykonawców + Błąd podczas pobierania radia wykonawcy + Błąd podczas pobierania utworów wykonawcy + Pobrani wykonawcy + Wykonawcy oznaczeni gwiazdką + Wykonawcy + Radio + Odtwarzanie losowe + Zmień układ + Więcej podobnych + Albumy + Więcej + Biografia + Najczęsciej słuchane utwory + Zobacz wszystkie + Ignoruj + Nie pytaj ponownie + Wyłącz + Anuluj + Włącz oszczędzanie danych + OK + Dostęp do serwera Subsonic na połaczeniach innych niż Wi-Fi został ograniczony. Aby zapobiec ponownemu pojawieniu się tej informacji, wyłącz sprawdzanie połączenia w ustawieniach aplikacji. + Nie połączono z Wi-Fi + Odtwarzanie losowe + Anuluj + Kontynuuj + Miej na uwadze to że kontynuowanie tej operacji spowoduje usunięcie wszystkich pobranych plików z wszystkich serwerów. + Usuwanie zapisanych plików + Brak opisu + Płyta %1$s - %2$s + Płyta %1$s + Anuluj + Pobierz + Wszystkie utwory w tym folderze zostaną pobrane. Utwory dostępne w subfolderach nie zostaną pobrane. + Pobierz utwory + Gdy pobierzesz piosenkę, znajdziesz ją tutaj + Narazie brak pobranych! + %1$s • %2$s elementów + %1$s element + Odtwórz losowo wszystkie + Aby zmiany przyniosły efekt, zrestartuj aplikację. + Zmiana lokalizacji pobieranych plików z jednej na drugą spowoduje natychmiastowe usunięcie wcześniej pobranych plików w drugiej lokalizacji + Wybieranie pamięci + Zewnętrzna + Wewnętrzna + Pobrane + Dodaj do kolejki + Odtwarzaj jako następne + Usuń + Usuń wszystkie + Odtwarzaj losowo + + Wymagane + wymagany jest prefiks http lub https + Pobieranie + Wybierz dwa lub więcej filtrów + Filtry + Filtruj Gatunki + (%1$d) + (+%1$d) + Katalog Gatunków + Przeglądaj Gatunki + Przypomnij mi później + Wesprzyj mnie + Pobierz teraz + Nowa wersja aplikacji jest dostępna na GitHubie. + Dostępna aktualizacja + Anuluj + Reset + Zapisz + Zmień układ strony głównej + Weź pod uwagę to że, żeby zmiany nastąpiły, musisz zrestartować aplikację. + Top piosenki od twoich ulubionych wykonawców + Stwórz miks z piosenki którą lubisz + Dodaj nowe radio + Dodaj nowy kanał podcastów + Anuluj + Pobierz + Pobieranie tych utworów może zużyć dużo danych + Wygląda na to że, są utwory oznaczone gwiazdką + Najlepsze + Odkrywanie + Odtwórz wszystkie losowo + Podróż w czasie + Internetowe stacje radiowe + Ostatnio odtwarzane + Zobacz wszystkie + Ostatni tydzień + Ostatni miesiąc + Ostatni rok + Stworzone dla ciebie + Najczęściej odtwarzane + Zobacz wszystkie + Nowe wydania + Najnowsze podcasty + Playlisty + Kanały + Zobacz wszystkie + Stacje radiowe + Ostatnio dodane + Zobacz wszystkie + Udostępnienia + ★ Albumy oznaczone gwiazdką + Zobacz wszystkie + ★ Wykonawcy oznaczeni gwiazdką + Zobacz wszystkich + ★ Utwory oznaczone gwiazdką + Zobacz wszystkie + Twoje top piosenki + Zmiana układu + + -- + Albumy + Zobacz wszystkie + Wykonawcy + Zobacz wszystkich + Gatunki + Zobacz wszystkie + Foldery z muzyką + Playlisty + Zobacz wszystkie + Brak dodanych serwerów + Serwery Subsonic + Serwery Subsonic + Przesyłanie + Dodaj + Pobierz wszystko + Pobrane + Wszystko + Pobrane + Albumy + Wykonawcy + Gatkunki + Utwory + Rok + Strona główna + Ostatni tydzień + Ostatni miesiąc + Ostatni rok + Biblioteka + Szukaj + Ustawienia + Wykonawca + Nazwa + Losowo + Ostatnio dodane + Ostatnio odtwarzane + Najczęściej odtwarzane + Ostatnio oznaczone największą liczbą gwiazdek + Ostatnio oznaczone najniższą liczbą gwiazdek + Dodaj do ekranu głównego + Usuń z ekranu głównego + Rok + %1$.2fx + Wyczyść kolejkę odtwarzania + Priorytet Serwerów + Katalog Playlist + Przeglądaj Playlisty + Nie utworzono playlist + Anuluj + Utwórz + Dodaj do playlisty + %1$d utworów • %2$s + Długość • %1$s + Przytrzymaj aby usunąć + Nazwa Playlisty + Anuluj + Usuń + Zapisz + Edytuj playlistę + Odtwórz + Odtwarzaj losowo + Playlista • %1$d piosenek + Dodaj do kolejki + Usuń + Pobierz + Przejdź do kanału + Odtwórz jako następny + Usuń + Kanały + Przeglądaj Kanały + Url RSS + Kanał Podcastu + Opis + Odcinki + Brak dostępnych odcinków + Twoje zapytanie zostało wysłane do serwera + Naciśnij aby ukryć tę sekcję\nEfekty będą widoczne po restarcie + Gdy dodasz kanał, znajdziesz go tutaj + Nie znaleziono podcastów! + %1$s • %2$s + URL Strony Radia + Nazwa Radia + URL Z Strumieniem Radia + Anuluj + Usuń + Zapisz + Internetowa Stacja Radiowa + Naciśnij aby ukryć tę sekcję\nEfekty będą widoczne po restarcie + Gdy dodasz stację radiową, znajdziesz ją tutaj + Nie znaleziono stacji! + Anuluj + Zapisz + Oceń + Wyszukaj tytuł, wykonawców lub albumy + Wpisz co najmniej trzy znaki + Albumy + Wykonawcy + Piosenki + Niskie bezpieczeństwo + Przytrzymaj aby usunąć + Lokalny URL + Nazwa Serwera + Hasło + URL Serwera + Nazwa użytkownika + Anuluj + Usuń + Zapisz + Dodaj serwer + Anuluj + Przejdź do logowania + Kontynuuj mimo wszystko + Wybrany serwer jest niedostępny. Jeżeli wybierzesz żeby kontynuować ta informacja nie będzie się wyświetlać przez następną godzinę. + Serwer jest niedostępny + Tempo jest otwarto-źródłowym i lekkim klientem muzycznym dla Subsonic, stworzonym i zbudowanym natywnie dla Androida. + O aplikacji + Always on display + Format transkodowania + Jeżeli włączone, Tempo nie będzię wymuszał pobierania utworu z ustawieniami transkodowania wybranymi poniżej. + Priorytetyzuj ustawienia serwera używanego do strumieniowania w pobieraniach + Jeżeli włączone, Tempo będzie pobierał transkodowane utwory. + Pobieraj transkodowane utwory + Jeżeli włączone, serwer bedzię odpytywany o przybliżoną długość utworu. + Szacuj długość treści + Format transkodowania dla pobierania + Format transkodowania w sieci komórkowej + Format transkodowania w sieci Wi-Fi + Jeżeli włączone, Tempo nie będzie wymuszał strumieniowania utworu z ustawieniami transkodowania wybranymi poniżej. + Priorytetyzuj ustawienia transkodowania serwera + Priorytet przy transkodowaniu utworu danego serwerowi + Strategia buforowania + Aby zmiany przyniosły efekt, musisz ręcznie zrestartować aplikację. + Pozwala muzyce odtwarzać się dalej po końcu playlisty, odtwarza podobne piosenki + Odtwarzanie bez przerwy + Rozmiar cache dla okładek + Aby zmniejszyć zużycie danych, unikaj pobierania okładek. + Ogranicz zużycie danych komórkowych + Zatwierdzenie nieodwracalnie usunie wszystkie zapisane elementy + Usuń zapisane elementy + Pamięć do pobierania + Zmień ustawienia audio + Equalizer + https://github.com/eddyizm/tempo + Śledź tworzenie aplikacji + GitHub + Rozdzielczość obrazów + Język + Wyloguj + Bitrate dla pobierania + Bitrate dla danych komórkowych + Bitrate dla Wi-Fi + Rozmiar plików cache dla mediów + Pokaż foldery z muzyką + Jeżeli włączone, widoczna będzie sekcja z folderami z muzyką. Weź pod uwagę że żeby funkcja nawigacji po folderach działała poprawnie, serwer musi wspierać tę funkcję. + Pokazuj podcasty + Jeżeli włączone, widoczna będzie sekcja z podcastami. Zrestartuj aplikację aby, zmiany przyniosły pełny efekt. + Pokaż jakość audio + Bitrate i format audio będzie pokazywany dla każdego utworu. + Pokaż oceny elementów + Jeżeli włączone, ocena elementów oraz czy jest oznaczony jako ulubiony będą pokazywane. + Timer synchronizacji + Jeżeli włączone, użytkownik będzie miał możliwość zapisania kolejki i będzie miał możliwość załadowania jej stanu przy otwarciu aplikacji. + Synchronizuj kolejkę odtwarzania dla tego użytkownika + Pokaż radio + Jeżeli włączone, widoczna będzie sekcja radia. Zrestartuj aplikację aby, zmiany przyniosły pełny efekt. + Tryb wzmocnienia głośności przy ponownym odtwarzaniu + Zaokrąglone rogi + Rozmiar rogów + Ustawia wielkość krzywizny kąta. + Jeżeli włączone, ustawia kąt krzywizny dla wszystkich renderowanych okładek. Zmiany przyniosą efekt po restarcie. + Skanuj bibliotekę + Włącz scrobbling muzyki + Włącz udostępnianie muzyki + Rozmiar cache dla strumieniowania + Pamięć cache dla strumieniowania + Ważne jest to że scrobbling polega też na byciu włączonym na serwerze aby otrzymywać te dane. + Podczas słuchania radia wykonawcy, natychmiastowego miksu albo podczas odtwarzania losowego, utwory poniżej określonej oceny użytkownika będą ignorowane. + Wzmocnienie głośności jest funkcją która pozwala tobie na ustawienia poziomu głośności dla utworów aby słuchanie brzmiało cały czas tak samo. To ustawienia działa tylko wtedy kiedy utwór zawiera potrzebne metadane. + Scrobbling jest funkcją która pozwala twojemu urządzeniu na wysyłanie informacji na temat piosenek których słuchasz do serwera muzyki. Te informacje pomagają tworzyć spersonalizowane rekomendacje na podstawie twojego gustu muzycznego. + Pozwala udostępnić użytkownikowi muzykę przez link. Ta funkcjonalność musi być wspierana i włączona na serwerze i jest ograniczona do pojedyńczych utworów, albumów i playlist. + Przywraca stan kolejki odtwarzania dla tego użytkownika. Zawiera utwory w kolejce, aktualnie odtwarzany utwór i pozycję w nim. Serwer musi wspierać tę funkcję. + %1$s \nAktualnie w użyciu: %2$s MiB + Priorytet dawany trybowi transkodowania. Jeżeli ustawiony na \"Odtwarzanie bezpośrednie\" bitrate pliku nie zostanie zmieniony. + Pobieraj transkdowane media. Jeżeli włączone, endpoint pobierania nie będzie używnany, poza następującymi ustawieniami. \n\n Jeżeli \"Format transkodowania dla pobierania\" jest ustawiony na \"Pobieranie bezpośrednie\" bitrate pliku nie zostanie zmieniony. + Kiedy plik jest transkodowany w locie, klient nie pokazuje zwykle długości utworu.Jest możliwe odpytanie serwera który wspiera tą funkcjonalność aby oszacował długość odtwarzanego utworu, ale czasy odpowiedzi mogą być dłuższe. + Jeżeli włączone, utwory oznaczone gwiazdką będą pobrane do użycia offline. + Zsynchronizuj utwory oznaczone gwiazdką do użycia offline + Motyw + Dane + Ogólne + Oceny + Wzmocnienie głośności przy ponownym odtwarzaniu + Scrobble + Ignoruj utwory na podstawie oceny + Piosenki z oceną: + Udostępnianie + Synchronizacja + Transkodowanie + Transkodowanie Pobrań + Interfejs + Transkodowane pobieranie + 3.1.0 + Wersja + Pytaj o potwierdzenie od użytkownika przed strumieniowaniem przez sieć komórkową. + Alert o strumieniowaniu tylko przez Wi-Fi + Kopiuj link + Usuń udostępnianie + Zaktualizuj udostępnianie + Data wygaśnięcia: %1$s + Udostępnianie nie jest wspierane lub włączone + Opis + Data wygaśnięcia + Anuluj + Zapisz + Udostępnij + Dodaj do playlisty + Dodaj do kolejki + Pobierz + Błąd podczas pobierania albumu + Błąd podczas pobierania wykonawcy + Przejdź do albumu + Przejdź do wykonawcy + Natychmiastowy miks + Odtwarzaj jako następne + Oceń + Usuń + Udostępnij + Pobrane + Najczęściej odtwarzane utwory + Utwory dodane ostatnio + Utwory odtwarzane ostatnio + Utwory oznaczone gwiazdką + %1$s\ top utwory + Rok %1$d + %1$s • %2$s %3$s + Anuluj + Kontynuuj + Kontynuuj i pobierz + Pobieranie utworów oznaczonych gwiazdką może wymagać dużej ilośći danych. + Synchronizuj utwory oznaczone gwiazdką + Aby zmiany przyniosły efekt, zrestartuj aplikację. + Zmiana lokalizacji plików cache z jednej na drugą spowoduje natychmiastowe usunięcie wcześniej pobranych plików cache w drugiej lokalizacji. + Wybieranie pamięci + Zewnętrzna + Wewnętrzna + https://buymeacoffee.com/a.cappiello + Album + Wykonawca + Bitrate + Typ Treści + OK + Informacje o utworze + Numer płyty + Długość + Gatunek + Ścieżka + Rozmiar + Sufiks + Plik został pobrany przy użyciu API Subsonic. Kodek i bitrate pliku pozostaje nie zmieniony względem pliku źródłowego. + Aplikacja poprosi serwer żeby transkodować plik i zmienić jego bitrate. Użytkownik poprosił o kodek %1$s, z bitratem %2$s. Wszystkie potencjalne zmiany w kodeku i bitratecie pliku w wybranym formacie będą wykonywane przez serwer, który może ale nie musi obsługiwać tych operacji. + Aplikacja będzie tylko odczytywać oryginalne pliki jakie daje serwer. Aplikacja będzie wyłącznie prosić serwer o pliki bez transkodowania z bitratem oryginalnego źródła. + \Jakość odtwarzanego pliku zależy od decyzji serwera. Aplikacja nie będzie monitorować wyboru kodeku i bitrateu dla jakiegokolwiek potencjalnego transkodowania. + Aplikacja poprosi serwer o zmodyfikowanie bitrateu pliku. Użytkownik poprosił o bitrate %1$s, ale kodek pliku źródłowego pozostanie ten sam. Wszystkie potencjalne zmiany w bitracie pliku w wybranym formacie będą wykonywane przez serwer, który może ale nie musi obsługiwać tych operacji. + Aplikacja poprosi serwer żeby transkodować plik. Kodek wybrany przez użytkownika to %1$s, ale bitrate pliku źródłowego pozostanie ten sam. Wszystkie potencjalne zmiany w kodeku pliku w wybranym formacie będą wykonywane przez serwer, który może ale nie musi obsługiwać tych operacji + Tytuł + Numer utworu + Typ transkodowanej treści + Sufiks transkodowania + Rok + unDraw + Specjalne podziękowania dla unDraw bez którego ilustracji nie mogliśmy uczynić tej aplikacji jeszcze piękniejszą. + https://undraw.co/ + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 12f04a31..7e4f2de7 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -3,6 +3,7 @@ Por favor, desative as otimizações de bateria para a reprodução de mídia enquanto a tela estiver desligada. Otimizações de bateria Modo offline + Adicionar a uma playlist Adicionar à fila Baixar todos Ir para o(a) artista @@ -134,6 +135,7 @@ Servidores Subsonic Transmitir Adicionar + Adicionar a uma playlist Baixar todos Download Todos @@ -160,6 +162,8 @@ Cancelar Criar Adicionar a uma playlist + Adicionada playlist de reprodução + Falha ao adicionar uma playlist de reprodução %1$d faixas • %2$s Duração • %1$s Nome da playlist @@ -246,7 +250,7 @@ Armazenamento dos downloads Ajustar configurações de áudio Equalizador - https://github.com/CappielloAntonio/tempo + https://github.com/eddyizm/tempo Acompanhe o desenvolvimento Github Definir resolução da imagem diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 0f893b90..43448d65 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -3,6 +3,7 @@ Пожалуйста, отключите оптимизацию батареи для воспроизведения мультимедиа при выключенном экране. Оптимизация батареи Офлайн-режим + Добавить в плейлист Добавить в очередь Скачать все Перейти к исполнителю @@ -153,6 +154,7 @@ Subsonic серверы Cast Добавить + Добавить в плейлист Скачать все Скачать Все @@ -186,6 +188,8 @@ Отмена Создать Добавить в плейлист + Добавьте песню в плейлист + Не удалось добавить песню в список воспроизведения %1$d треков • %2$s Продолжительность • %1$s Долгое нажатие для удаления @@ -275,7 +279,7 @@ Загрузить хранилище Отрегулируйте настройки звука Эквалайзер - https://github.com/CappielloAntonio/tempo + https://github.com/eddyizm/tempo Следите за развитием Github Установить разрешение изображения diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 915b33d1..b0933d57 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -3,6 +3,7 @@ 请禁用针对媒体锁屏播放的电池优化。 电池优化 离线模式 + 添加到播放列表 添加到队列 全部下载 查看该艺术家 @@ -137,6 +138,7 @@ Subsonic 服务器 投送 添加 + 添加到播放列表 全部下载 下载 全部 @@ -165,6 +167,8 @@ 取消 新建 添加到播放列表 + 将歌曲添加到播放列表 + 未能将歌曲添加到播放列表 %1$d 首曲目 • %2$s 持续时间 • %1$s 播放列表名称 @@ -253,7 +257,7 @@ 下载存储 调整音频设置 均衡器 - https://github.com/CappielloAntonio/tempo + https://github.com/eddyizm/tempo 关注开发进展 Github 设置图像分辨率 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5fd1ac33..8ecb78af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,6 +3,7 @@ Please disable battery optimizations for media playback while the screen is off. Battery Optimizations Offline mode + Add to playlist Add to queue Download all Go to artist @@ -158,6 +159,7 @@ Subsonic servers Cast Add + Add to playlist Download all Download All @@ -194,6 +196,8 @@ Cancel Create Add to a playlist + Added song to playlist + Failed to add song to playlist %1$d tracks • %2$s Duration • %1$s Long press to delete @@ -285,7 +289,7 @@ Download storage Adjust audio settings Equalizer - https://github.com/CappielloAntonio/tempo + https://github.com/eddyizm/tempo Follow the development Github Set image resolution diff --git a/app/src/main/res/xml/locale_config.xml b/app/src/main/res/xml/locale_config.xml index 9e9c43d1..ac6e6d0e 100644 --- a/app/src/main/res/xml/locale_config.xml +++ b/app/src/main/res/xml/locale_config.xml @@ -9,4 +9,5 @@ + diff --git a/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt index 7125b9fd..b8a7179b 100644 --- a/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -32,7 +32,8 @@ class MediaService : MediaLibraryService() { private lateinit var player: ExoPlayer private lateinit var mediaLibrarySession: MediaLibrarySession - private lateinit var customCommands: List + private lateinit var shuffleCommands: List + private lateinit var repeatCommands: List private var customLayout = ImmutableList.of() @@ -41,6 +42,12 @@ class MediaService : MediaLibraryService() { "android.media3.session.demo.SHUFFLE_ON" private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF = "android.media3.session.demo.SHUFFLE_OFF" + private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF = + "android.media3.session.demo.REPEAT_OFF" + private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE = + "android.media3.session.demo.REPEAT_ONE" + private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = + "android.media3.session.demo.REPEAT_ALL" } override fun onCreate() { @@ -72,7 +79,7 @@ class MediaService : MediaLibraryService() { val connectionResult = super.onConnect(session, controller) val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon() - customCommands.forEach { commandButton -> + shuffleCommands.forEach { commandButton -> // TODO: Aggiungere i comandi personalizzati // commandButton.sessionCommand?.let { availableSessionCommands.add(it) } } @@ -89,22 +96,40 @@ class MediaService : MediaLibraryService() { } } + fun buildCustomLayout(player: Player): ImmutableList { + val shuffle = shuffleCommands[if (player.shuffleModeEnabled) 1 else 0] + val repeat = when (player.repeatMode) { + Player.REPEAT_MODE_ONE -> repeatCommands[1] + Player.REPEAT_MODE_ALL -> repeatCommands[2] + else -> repeatCommands[0] + } + return ImmutableList.of(shuffle, repeat) + } + override fun onCustomCommand( session: MediaSession, controller: ControllerInfo, customCommand: SessionCommand, args: Bundle ): ListenableFuture { - if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) { - player.shuffleModeEnabled = true - customLayout = ImmutableList.of(customCommands[1]) - session.setCustomLayout(customLayout) - } else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) { - player.shuffleModeEnabled = false - customLayout = ImmutableList.of(customCommands[0]) - session.setCustomLayout(customLayout) + when (customCommand.customAction) { + CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> player.shuffleModeEnabled = true + CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> player.shuffleModeEnabled = false + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF, + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL, + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> { + val nextMode = when (player.repeatMode) { + Player.REPEAT_MODE_ONE -> Player.REPEAT_MODE_ALL + Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ONE + else -> Player.REPEAT_MODE_OFF + } + player.repeatMode = nextMode + } } + customLayout = librarySessionCallback.buildCustomLayout(player) + session.setCustomLayout(customLayout) + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } @@ -125,17 +150,28 @@ class MediaService : MediaLibraryService() { } private fun initializeCustomCommands() { - customCommands = - listOf( - getShuffleCommandButton( - SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY) - ), - getShuffleCommandButton( - SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY) - ) + shuffleCommands = listOf( + getShuffleCommandButton( + SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY) + ), + getShuffleCommandButton( + SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY) ) + ) - customLayout = ImmutableList.of(customCommands[0]) + repeatCommands = listOf( + getRepeatCommandButton( + SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF, Bundle.EMPTY) + ), + getRepeatCommandButton( + SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE, Bundle.EMPTY) + ), + getRepeatCommandButton( + SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL, Bundle.EMPTY) + ) + ) + + customLayout = ImmutableList.of(shuffleCommands[0], repeatCommands[0]) } private fun initializePlayer() { @@ -147,6 +183,9 @@ class MediaService : MediaLibraryService() { .setWakeMode(C.WAKE_MODE_NETWORK) .setLoadControl(initializeLoadControl()) .build() + + player.shuffleModeEnabled = Preferences.isShuffleModeEnabled() + player.repeatMode = Preferences.getRepeatMode() } private fun initializeMediaLibrarySession() { @@ -224,6 +263,18 @@ class MediaService : MediaLibraryService() { } } } + + override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { + Preferences.setShuffleModeEnabled(shuffleModeEnabled) + customLayout = librarySessionCallback.buildCustomLayout(player) + mediaLibrarySession.setCustomLayout(customLayout) + } + + override fun onRepeatModeChanged(repeatMode: Int) { + Preferences.setRepeatMode(repeatMode) + customLayout = librarySessionCallback.buildCustomLayout(player) + mediaLibrarySession.setCustomLayout(customLayout) + } }) } @@ -251,6 +302,25 @@ class MediaService : MediaLibraryService() { .build() } + @SuppressLint("PrivateResource") + private fun getRepeatCommandButton(sessionCommand: SessionCommand): CommandButton { + val icon = when (sessionCommand.customAction) { + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> R.drawable.exo_icon_repeat_one + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL -> R.drawable.exo_icon_repeat_all + else -> R.drawable.exo_icon_repeat_off + } + val description = when (sessionCommand.customAction) { + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> R.string.exo_controls_repeat_one_description + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL -> R.string.exo_controls_repeat_all_description + else -> R.string.exo_controls_repeat_off_description + } + return CommandButton.Builder() + .setDisplayName(getString(description)) + .setSessionCommand(sessionCommand) + .setIconResId(icon) + .build() + } + private fun ignoreFuture(customLayout: ListenableFuture) { /* Do nothing. */ } diff --git a/app/src/play/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt b/app/src/play/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt index 747a45b2..099ae672 100644 --- a/app/src/play/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt +++ b/app/src/play/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt @@ -4,6 +4,7 @@ import android.content.Context import android.os.Bundle import androidx.annotation.OptIn import androidx.media3.common.MediaItem +import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.session.CommandButton import androidx.media3.session.LibraryResult @@ -27,7 +28,7 @@ open class MediaLibrarySessionCallback( MediaBrowserTree.initialize(automotiveRepository) } - private val customLayoutCommandButtons: List = listOf( + private val shuffleCommandButtons: List = listOf( CommandButton.Builder() .setDisplayName(context.getString(R.string.exo_controls_shuffle_on_description)) .setSessionCommand( @@ -45,15 +46,46 @@ open class MediaLibrarySessionCallback( ).setIconResId(R.drawable.exo_icon_shuffle_on).build() ) + private val repeatCommandButtons: List = listOf( + CommandButton.Builder() + .setDisplayName(context.getString(R.string.exo_controls_repeat_off_description)) + .setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF, Bundle.EMPTY)) + .setIconResId(R.drawable.exo_icon_repeat_off) + .build(), + CommandButton.Builder() + .setDisplayName(context.getString(R.string.exo_controls_repeat_one_description)) + .setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE, Bundle.EMPTY)) + .setIconResId(R.drawable.exo_icon_repeat_one) + .build(), + CommandButton.Builder() + .setDisplayName(context.getString(R.string.exo_controls_repeat_all_description)) + .setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL, Bundle.EMPTY)) + .setIconResId(R.drawable.exo_icon_repeat_all) + .build() + ) + + private val customLayoutCommandButtons: List = + shuffleCommandButtons + repeatCommandButtons + @OptIn(UnstableApi::class) val mediaNotificationSessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon() .also { builder -> - customLayoutCommandButtons.forEach { commandButton -> + (shuffleCommandButtons + repeatCommandButtons).forEach { commandButton -> commandButton.sessionCommand?.let { builder.add(it) } } }.build() + fun buildCustomLayout(player: Player): ImmutableList { + val shuffle = shuffleCommandButtons[if (player.shuffleModeEnabled) 1 else 0] + val repeat = when (player.repeatMode) { + Player.REPEAT_MODE_ONE -> repeatCommandButtons[1] + Player.REPEAT_MODE_ALL -> repeatCommandButtons[2] + else -> repeatCommandButtons[0] + } + return ImmutableList.of(shuffle, repeat) + } + @OptIn(UnstableApi::class) override fun onConnect( session: MediaSession, controller: MediaSession.ControllerInfo @@ -62,12 +94,11 @@ open class MediaLibrarySessionCallback( controller ) || session.isAutoCompanionController(controller) ) { - val customLayout = - customLayoutCommandButtons[if (session.player.shuffleModeEnabled) 1 else 0] + val customLayout = buildCustomLayout(session.player) return MediaSession.ConnectionResult.AcceptedResultBuilder(session) .setAvailableSessionCommands(mediaNotificationSessionCommands) - .setCustomLayout(ImmutableList.of(customLayout)).build() + .setCustomLayout(customLayout).build() } return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build() @@ -80,25 +111,28 @@ open class MediaLibrarySessionCallback( customCommand: SessionCommand, args: Bundle ): ListenableFuture { - if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) { - session.player.shuffleModeEnabled = true - session.setCustomLayout( - session.mediaNotificationControllerInfo!!, - ImmutableList.of(customLayoutCommandButtons[1]) - ) - - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) - } else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) { - session.player.shuffleModeEnabled = false - session.setCustomLayout( - session.mediaNotificationControllerInfo!!, - ImmutableList.of(customLayoutCommandButtons[0]) - ) - - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + when (customCommand.customAction) { + CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> session.player.shuffleModeEnabled = true + CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> session.player.shuffleModeEnabled = false + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF, + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL, + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> { + val nextMode = when (session.player.repeatMode) { + Player.REPEAT_MODE_ONE -> Player.REPEAT_MODE_ALL + Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ONE + else -> Player.REPEAT_MODE_OFF + } + session.player.repeatMode = nextMode + } + else -> return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)) } - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)) + session.setCustomLayout( + session.mediaNotificationControllerInfo!!, + buildCustomLayout(session.player) + ) + + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } override fun onGetLibraryRoot( @@ -158,5 +192,11 @@ open class MediaLibrarySessionCallback( "android.media3.session.demo.SHUFFLE_ON" private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF = "android.media3.session.demo.SHUFFLE_OFF" + private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF = + "android.media3.session.demo.REPEAT_OFF" + private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE = + "android.media3.session.demo.REPEAT_ONE" + private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = + "android.media3.session.demo.REPEAT_ALL" } } \ No newline at end of file diff --git a/app/src/play/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/play/java/com/cappielloantonio/tempo/service/MediaService.kt index b993aaba..2391a2bb 100644 --- a/app/src/play/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/play/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -33,6 +33,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { private lateinit var player: ExoPlayer private lateinit var castPlayer: CastPlayer private lateinit var mediaLibrarySession: MediaLibrarySession + private lateinit var librarySessionCallback: MediaLibrarySessionCallback override fun onCreate() { super.onCreate() @@ -79,6 +80,9 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { .setWakeMode(C.WAKE_MODE_NETWORK) .setLoadControl(initializeLoadControl()) .build() + + player.shuffleModeEnabled = Preferences.isShuffleModeEnabled() + player.repeatMode = Preferences.getRepeatMode() } private fun initializeCastPlayer() { @@ -97,13 +101,14 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT) } + librarySessionCallback = createLibrarySessionCallback() mediaLibrarySession = - MediaLibrarySession.Builder(this, player, createLibrarySessionCallback()) + MediaLibrarySession.Builder(this, player, librarySessionCallback) .setSessionActivity(sessionActivityPendingIntent) .build() } - private fun createLibrarySessionCallback(): MediaLibrarySession.Callback { + private fun createLibrarySessionCallback(): MediaLibrarySessionCallback { return MediaLibrarySessionCallback(this, automotiveRepository) } @@ -166,6 +171,20 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { } } } + + override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { + Preferences.setShuffleModeEnabled(shuffleModeEnabled) + mediaLibrarySession.setCustomLayout( + librarySessionCallback.buildCustomLayout(player) + ) + } + + override fun onRepeatModeChanged(repeatMode: Int) { + Preferences.setRepeatMode(repeatMode) + mediaLibrarySession.setCustomLayout( + librarySessionCallback.buildCustomLayout(player) + ) + } }) } diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt index 747a45b2..099ae672 100644 --- a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt +++ b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt @@ -4,6 +4,7 @@ import android.content.Context import android.os.Bundle import androidx.annotation.OptIn import androidx.media3.common.MediaItem +import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.session.CommandButton import androidx.media3.session.LibraryResult @@ -27,7 +28,7 @@ open class MediaLibrarySessionCallback( MediaBrowserTree.initialize(automotiveRepository) } - private val customLayoutCommandButtons: List = listOf( + private val shuffleCommandButtons: List = listOf( CommandButton.Builder() .setDisplayName(context.getString(R.string.exo_controls_shuffle_on_description)) .setSessionCommand( @@ -45,15 +46,46 @@ open class MediaLibrarySessionCallback( ).setIconResId(R.drawable.exo_icon_shuffle_on).build() ) + private val repeatCommandButtons: List = listOf( + CommandButton.Builder() + .setDisplayName(context.getString(R.string.exo_controls_repeat_off_description)) + .setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF, Bundle.EMPTY)) + .setIconResId(R.drawable.exo_icon_repeat_off) + .build(), + CommandButton.Builder() + .setDisplayName(context.getString(R.string.exo_controls_repeat_one_description)) + .setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE, Bundle.EMPTY)) + .setIconResId(R.drawable.exo_icon_repeat_one) + .build(), + CommandButton.Builder() + .setDisplayName(context.getString(R.string.exo_controls_repeat_all_description)) + .setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL, Bundle.EMPTY)) + .setIconResId(R.drawable.exo_icon_repeat_all) + .build() + ) + + private val customLayoutCommandButtons: List = + shuffleCommandButtons + repeatCommandButtons + @OptIn(UnstableApi::class) val mediaNotificationSessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon() .also { builder -> - customLayoutCommandButtons.forEach { commandButton -> + (shuffleCommandButtons + repeatCommandButtons).forEach { commandButton -> commandButton.sessionCommand?.let { builder.add(it) } } }.build() + fun buildCustomLayout(player: Player): ImmutableList { + val shuffle = shuffleCommandButtons[if (player.shuffleModeEnabled) 1 else 0] + val repeat = when (player.repeatMode) { + Player.REPEAT_MODE_ONE -> repeatCommandButtons[1] + Player.REPEAT_MODE_ALL -> repeatCommandButtons[2] + else -> repeatCommandButtons[0] + } + return ImmutableList.of(shuffle, repeat) + } + @OptIn(UnstableApi::class) override fun onConnect( session: MediaSession, controller: MediaSession.ControllerInfo @@ -62,12 +94,11 @@ open class MediaLibrarySessionCallback( controller ) || session.isAutoCompanionController(controller) ) { - val customLayout = - customLayoutCommandButtons[if (session.player.shuffleModeEnabled) 1 else 0] + val customLayout = buildCustomLayout(session.player) return MediaSession.ConnectionResult.AcceptedResultBuilder(session) .setAvailableSessionCommands(mediaNotificationSessionCommands) - .setCustomLayout(ImmutableList.of(customLayout)).build() + .setCustomLayout(customLayout).build() } return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build() @@ -80,25 +111,28 @@ open class MediaLibrarySessionCallback( customCommand: SessionCommand, args: Bundle ): ListenableFuture { - if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) { - session.player.shuffleModeEnabled = true - session.setCustomLayout( - session.mediaNotificationControllerInfo!!, - ImmutableList.of(customLayoutCommandButtons[1]) - ) - - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) - } else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) { - session.player.shuffleModeEnabled = false - session.setCustomLayout( - session.mediaNotificationControllerInfo!!, - ImmutableList.of(customLayoutCommandButtons[0]) - ) - - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + when (customCommand.customAction) { + CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> session.player.shuffleModeEnabled = true + CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> session.player.shuffleModeEnabled = false + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF, + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL, + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> { + val nextMode = when (session.player.repeatMode) { + Player.REPEAT_MODE_ONE -> Player.REPEAT_MODE_ALL + Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ONE + else -> Player.REPEAT_MODE_OFF + } + session.player.repeatMode = nextMode + } + else -> return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)) } - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)) + session.setCustomLayout( + session.mediaNotificationControllerInfo!!, + buildCustomLayout(session.player) + ) + + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } override fun onGetLibraryRoot( @@ -158,5 +192,11 @@ open class MediaLibrarySessionCallback( "android.media3.session.demo.SHUFFLE_ON" private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF = "android.media3.session.demo.SHUFFLE_OFF" + private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF = + "android.media3.session.demo.REPEAT_OFF" + private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE = + "android.media3.session.demo.REPEAT_ONE" + private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = + "android.media3.session.demo.REPEAT_ALL" } } \ No newline at end of file diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt index b993aaba..2391a2bb 100644 --- a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -33,6 +33,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { private lateinit var player: ExoPlayer private lateinit var castPlayer: CastPlayer private lateinit var mediaLibrarySession: MediaLibrarySession + private lateinit var librarySessionCallback: MediaLibrarySessionCallback override fun onCreate() { super.onCreate() @@ -79,6 +80,9 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { .setWakeMode(C.WAKE_MODE_NETWORK) .setLoadControl(initializeLoadControl()) .build() + + player.shuffleModeEnabled = Preferences.isShuffleModeEnabled() + player.repeatMode = Preferences.getRepeatMode() } private fun initializeCastPlayer() { @@ -97,13 +101,14 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT) } + librarySessionCallback = createLibrarySessionCallback() mediaLibrarySession = - MediaLibrarySession.Builder(this, player, createLibrarySessionCallback()) + MediaLibrarySession.Builder(this, player, librarySessionCallback) .setSessionActivity(sessionActivityPendingIntent) .build() } - private fun createLibrarySessionCallback(): MediaLibrarySession.Callback { + private fun createLibrarySessionCallback(): MediaLibrarySessionCallback { return MediaLibrarySessionCallback(this, automotiveRepository) } @@ -166,6 +171,20 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { } } } + + override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { + Preferences.setShuffleModeEnabled(shuffleModeEnabled) + mediaLibrarySession.setCustomLayout( + librarySessionCallback.buildCustomLayout(player) + ) + } + + override fun onRepeatModeChanged(repeatMode: Int) { + Preferences.setRepeatMode(repeatMode) + mediaLibrarySession.setCustomLayout( + librarySessionCallback.buildCustomLayout(player) + ) + } }) } diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/settings.gradle b/settings.gradle index a7c6dd41..1cd079ae 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,5 @@ +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' +} include ':app' rootProject.name = "Tempo" \ No newline at end of file