37 Commits

Author SHA1 Message Date
eddyizm
fe2c163aaa bug fixes, chores, docs v3.14.8 (#80) 2025-08-31 12:43:06 -07:00
eddyizm
c6f08d9cec chore: started usage docs, added a couple of discussion items, created layout 2025-08-31 12:38:13 -07:00
eddyizm
59b40df9ef fix: Disable "sync starred tracks/albums" switches when Cancel is clicked in warning dialog, use proper view for "Sync starred albums" dialog (#79) 2025-08-31 12:36:35 -07:00
eddyizm
81726baa08 style: Center subtitle text in empty_download_layout in fragment_download.xml when there is more than one line (#78) 2025-08-31 12:36:15 -07:00
eddyizm
af92a7b11a chore(i18n): Update Spanish (es-ES) and English translations (#77) 2025-08-31 12:35:40 -07:00
Jaime García
0578745bee fix: use "DialogStarredAlbumSync" view binding instead of "DialogStarredSync" 2025-08-31 17:48:56 +02:00
Jaime García
e24063e460 fix: disable sync starred tracks/albums switches in when Cancel is clicked in warning dialog 2025-08-31 17:39:35 +02:00
Jaime García
4f1b1b603e style: center subtitle text in empty_download_layout in fragment_download.xml when there is more than one line 2025-08-31 17:05:27 +02:00
Jaime García
7279c62944 chore(i18n): update Spanish translation 2025-08-31 16:44:00 +02:00
eddyizm
a59d46f884 Merge branch 'development' of github.com:eddyizm/tempo into development 2025-08-31 07:39:22 -07:00
eddyizm
10285b308d fix: Use correct SearchView widget to avoid crash in AlbumListPageFragment (#76) 2025-08-31 07:38:04 -07:00
Jaime García
8e2c5d1fee chore(i18n): update English translation 2025-08-31 16:32:01 +02:00
Jaime García
f59a360eb7 fix: Use correct SearchView widget to avoid crash in AlbumListPageFragment 2025-08-31 15:32:56 +02:00
eddyizm
accf5fddc2 chore: changelog update 2025-08-30 11:36:42 -07:00
eddyizm
cc61d1cd48 v3.14.1 release (#74) 2025-08-30 11:00:57 -07:00
eddyizm
6a16159cf0 fix: forgot sync album dialog, bump version for release 2025-08-30 10:58:38 -07:00
eddyizm
eaf2710054 fix: minor refactor of sync album observer 2025-08-30 09:23:44 -07:00
eddyizm
31d91f7215 feat: adds sync starred albums functionality #66 (#73) 2025-08-30 09:06:47 -07:00
eddyizm
f854f49686 feat: adds sync starred albums functionality #66 2025-08-30 09:04:25 -07:00
eddyizm
9e8870a86a Update French localization (#70) 2025-08-29 14:11:23 -07:00
Benoît Smith
a0040c52a0 More updates for strings.xml (FR) 2025-08-28 11:59:57 +02:00
Benoît Smith
65f6347faf Merge branch 'eddyizm:development' into development 2025-08-28 11:18:12 +02:00
Benoît Smith
85fa2f768e Update strings.xml (FR) 2025-08-28 11:17:32 +02:00
eddyizm
cc5abd150a fix: artist filtering in library view browse artist resolves #45 (#69) 2025-08-27 18:32:46 -07:00
eddyizm
1ed6ac6cff fix: artist filtering in library view browse artist resolves #45 2025-08-27 18:29:14 -07:00
eddyizm
cc6cb077b4 fix: catches null value and prepares bundle appropriately adding sing… (#64) 2025-08-26 21:59:33 -07:00
eddyizm
4be0acf76c fix: catches null value and prepares bundle appropriately adding single track to expected array list. closes #58 2025-08-26 21:57:20 -07:00
eddyizm
7d843390db setting-to-hide-song-rating (#60) 2025-08-25 20:49:30 -07:00
eddyizm
5c5316055c feat: setting to show/hide 5 star rating on playerview (#59)
First pass, I was not able to get the setting to update without having to restart the app. My attempt at using live data was missing something so I will have to revisit this when I get a better hang of it.
2025-08-25 20:42:09 -07:00
eddyizm
f1a179e7f8 Merge branch 'development' into setting-to-hide-song-rating 2025-08-24 19:14:36 -07:00
eddyizm
0377c5e939 feat: setting to show/hide 5 star rating on playerview 2025-08-24 18:55:36 -07:00
eddyizm
614ce8b466 style: Add song rating bar in landscape player controller layout (#57) 2025-08-24 16:33:50 -07:00
eddyizm
9e87b53bc9 feat: rating dialog added to album page (#52) 2025-08-24 16:32:50 -07:00
Jaime García
08023026b4 style: Add song rating bar in landscape player controller layout 2025-08-24 19:24:05 +02:00
eddyizm
1bbcf6c790 feat: rating dialog added to album page 2025-08-23 17:57:45 -07:00
eddyizm
698ca3b22b chore: changelog update 2025-08-23 14:05:01 -07:00
eddyizm
b2d875ac98 Merge pull request #51 from eddyizm/development
Development
2025-08-23 13:52:44 -07:00
34 changed files with 800 additions and 28 deletions

View File

@@ -2,6 +2,33 @@
***This log is for this fork to detail updates since 3.9.0 from the main repo.***
## [3.14.1](https://github.com/eddyizm/tempo/releases/tag/v3.14.1) (2025-08-30)
## What's Changed
* feat: rating dialog added to album page by @eddyizm in https://github.com/eddyizm/tempo/pull/52
* style: Add song rating bar in landscape player controller layout by @jaime-grj in https://github.com/eddyizm/tempo/pull/57
* feat: setting to show/hide 5 star rating on playerview by @eddyizm in https://github.com/eddyizm/tempo/pull/59
* chore: setting-to-hide-song-rating by @eddyizm in https://github.com/eddyizm/tempo/pull/60
* fix: catches null value and prepares bundle appropriately adding sing… by @eddyizm in https://github.com/eddyizm/tempo/pull/64
* fix: artist filtering in library view browse artist resolves #45 by @eddyizm in https://github.com/eddyizm/tempo/pull/69
* chore: Update French localization by @benoit-smith in https://github.com/eddyizm/tempo/pull/70
* feat: adds sync starred albums functionality #66 by @eddyizm in https://github.com/eddyizm/tempo/pull/73
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.13.0...v3.14.1
## [3.13.0](https://github.com/eddyizm/tempo/releases/tag/v3.13.0) (2025-08-23)
## What's Changed
* style: Change position and size of rating container by @jaime-grj in https://github.com/eddyizm/tempo/pull/44
* feat: Add Turkish localization (values-tr) by @mucahit-kaya in https://github.com/eddyizm/tempo/pull/50
* chore: adding a note/not fully baked label to the sync user play queue setting by @eddyizm in https://github.com/eddyizm/tempo/commit/8ed0a4642bd0cd637c65e3115142596331fa7ef7
* fix: moved hardcoded italian save text to string template, updated with english and italian language xmls by @eddyizm in https://github.com/eddyizm/tempo/commit/26a5fb029a07752c9c0db0d08a89afd638772579
## New Contributors
* @mucahit-kaya made their first contribution in https://github.com/eddyizm/tempo/pull/50
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.12.0...v3.13.0
## [3.12.0](https://github.com/eddyizm/tempo/releases/tag/v3.12.0) (2025-08-15)
### What's Changed
* [chore]: add German translations for track info and home section strings (#29) by @BreadWare92 in https://github.com/eddyizm/tempo/pull/31

View File

@@ -30,6 +30,10 @@ Moved details to [CHANGELOG.md](https://github.com/eddyizm/tempo/blob/main/CHANG
Fork [**sponsorship here**](https://ko-fi.com/eddyizm).
## Usage
[Documentation](USAGE.md) (work in progress)
## 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.

146
USAGE.md Normal file
View File

@@ -0,0 +1,146 @@
# App Name Usage Guide
[<- back home](README.md)
## Table of Contents
- [Prerequisites](#prerequisites)
- [Getting Started](#getting-started)
- [Server Configuration](#server-configuration)
- [Main Features](#main-features)
- [Navigation](#navigation)
- [Playback Controls](#playback-controls)
- [Favorites](#favorites)
- [Playlist Management](#playlist-management)
- [Android Auto](#android-auto)
- [Settings](#settings)
- [Troubleshooting](#troubleshooting)
## Prerequisites
**Important Notice**: This app is a Subsonic-compatible client and does not provide any music content itself. To use this application, you must have:
- An active Subsonic API server (or compatible service) already set up
- Valid login credentials for your Subsonic server
- Music content uploaded and organized on your server
### Verified backends
This app works with any service that implements the Subsonic API, including:
- [LMS - Lightweight Music Server](https://github.com/epoupon/lms) - *personal fave and my backend*
- [Navidrome](https://www.navidrome.org/)
- [Gonic](https://github.com/sentriz/gonic)
## Getting Started
### Installation
1. Download the APK from the [Releases](https://github.com/eddyizm/tempo/releases) section
2. Enable "Install from unknown sources" in your Android settings
3. Install the application
### First Launch
1. Open the application
2. You will be prompted to configure your server connection
3. Grant necessary permissions for media playback and background operation
## Server Configuration
### Initial Setup
**IN PROGRESS**
1. Enter your server URL (e.g., `https://your-subsonic-server.com`)
2. Provide your username and password
3. Test the connection to ensure proper configuration
### Advanced Settings
**TODO**
## Main Features
### Library View
**TODO**
### Now Playing Screen
**TODO**
## Navigation
### Bottom Navigation Bar
**IN PROGRESS**
- **Home**: Recently played and server recommendations
- **Library**: Your server's complete music collection
- **Download**: Locally downloaded files from server
## Playback Controls
### Streaming Controls
**TODO**
### Advanced Controls
**TODO**
## Favorites
### Favorites (aka heart aka star) to albums and artists
- Long pressing on an album gives you access to heart/unheart an album
<p align="center">
<img src="mockup/usage/fave_album.png" width=376>
</p>
- Long pressing on an artist cover gets you the same access to to heart/unheart an album
<p align="center">
<img src="mockup/usage/fave_artist.png" width=376>
</p>
## Playlist Management
### Server Playlists
**TODO**
### Creating Playlists
**TODO**
## Settings
## Android Auto
### Enabling on your head unit
- You have to enable Android Auto developer options, which are different from actual Android dev options. Then you have to enable "Unknown sources" in Android Auto, otherwise the app won't appear as it isn't downloaded from Play Store. (screenshots needed)
### Server Settings
**IN PROGRESS**
- Manage multiple server connections
- Configure sync intervals
- Set data usage limits for streaming
### Audio Settings
**IN PROGRESS**
- Streaming quality settings
- Offline caching preferences
### Appearance
**TODO**
## Troubleshooting
### Connection Issues
**TODO**
### Common Issues
**TODO**
### Support
For additional help:
- Question? Start a [Discussion](https://github.com/eddyizm/tempo/discussions)
- Open an [issue](https://github.com/eddyizm/tempo/issues) if you don't find a discussion solving your issue.
- Consult your Subsonic server's documentation
---
*Note: This app requires a pre-existing Subsonic-compatible server with music content.*

View File

@@ -10,8 +10,8 @@ android {
minSdkVersion 24
targetSdk 35
versionCode 29
versionName '3.13.0'
versionCode 30
versionName '3.14.1'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

@@ -316,6 +316,7 @@ public class MainActivity extends BaseActivity {
Preferences.setSkipSilenceMode(false);
Preferences.setDataSavingMode(false);
Preferences.setStarredSyncEnabled(false);
Preferences.setStarredAlbumsSyncEnabled(false);
}
private void resetMusicSession() {

View File

@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.ui.dialog;
import android.app.Dialog;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
@@ -97,8 +98,12 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba
@Override
public void onPlaylistClick(Bundle bundle) {
Playlist playlist = bundle.getParcelable(Constants.PLAYLIST_OBJECT);
playlistChooserViewModel.addSongsToPlaylist(playlist.getId());
dismiss();
if (playlistChooserViewModel.getSongsToAdd() != null && !playlistChooserViewModel.getSongsToAdd().isEmpty()) {
Playlist playlist = bundle.getParcelable(Constants.PLAYLIST_OBJECT);
playlistChooserViewModel.addSongsToPlaylist(playlist.getId());
dismiss();
} else {
Toast.makeText(requireContext(), R.string.playlist_chooser_dialog_toast_add_failure, Toast.LENGTH_SHORT).show();
}
}
}

View File

@@ -63,7 +63,11 @@ public class RatingDialog extends DialogFragment {
bind.ratingBar.setRating(song.getUserRating() != null ? song.getUserRating() : 0);
});
} else if (ratingViewModel.getAlbum() != null) {
ratingViewModel.getLiveAlbum().observe(this, album -> bind.ratingBar.setRating(/*album.getRating()*/ 0));
ratingViewModel.getLiveAlbum().observe(this, album -> {
if (album != null) {
bind.ratingBar.setRating(album.getUserRating() != null ? album.getUserRating() : 0);
}
});
} else if (ratingViewModel.getArtist() != null) {
ratingViewModel.getLiveArtist().observe(this, artist -> bind.ratingBar.setRating(/*artist.getRating()*/ 0));
}

View File

@@ -0,0 +1,88 @@
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.DialogStarredAlbumSyncBinding;
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;
private Runnable onCancel;
public StarredAlbumSyncDialog(Runnable onCancel) {
this.onCancel = onCancel;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
DialogStarredAlbumSyncBinding bind = DialogStarredAlbumSyncBinding.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);
if (onCancel != null) onCancel.run();
dialog.dismiss();
});
}
}
}

View File

@@ -26,6 +26,12 @@ import java.util.stream.Collectors;
public class StarredSyncDialog extends DialogFragment {
private StarredSyncViewModel starredSyncViewModel;
private Runnable onCancel;
public StarredSyncDialog(Runnable onCancel) {
this.onCancel = onCancel;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
@@ -75,6 +81,7 @@ public class StarredSyncDialog extends DialogFragment {
Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE);
negativeButton.setOnClickListener(v -> {
Preferences.setStarredSyncEnabled(false);
if (onCancel != null) onCancel.run();
dialog.dismiss();
});
}

View File

@@ -12,7 +12,7 @@ import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.PopupMenu;
import android.widget.SearchView;
import androidx.appcompat.widget.SearchView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -145,7 +145,7 @@ public class AlbumListPageFragment extends Fragment implements ClickCallback {
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.toolbar_menu, menu);
inflater.inflate(R.menu.artist_list_menu, menu);
MenuItem searchItem = menu.findItem(R.id.action_search);

View File

@@ -4,6 +4,7 @@ import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -27,11 +28,13 @@ import com.cappielloantonio.tempo.databinding.FragmentAlbumPageBinding;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil;
@@ -104,6 +107,16 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.action_rate_album) {
Bundle bundle = new Bundle();
AlbumID3 album = albumPageViewModel.getAlbum().getValue();
bundle.putParcelable(Constants.ALBUM_OBJECT, (Parcelable) album);
RatingDialog dialog = new RatingDialog();
dialog.setArguments(bundle);
dialog.show(requireActivity().getSupportFragmentManager(), null);
return true;
}
if (item.getItemId() == R.id.action_download_album) {
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> {
DownloadUtil.getDownloadTracker(requireContext()).download(MappingUtil.mapDownloads(songs), songs.stream().map(Download::new).collect(Collectors.toList()));

View File

@@ -13,6 +13,7 @@ import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.PopupMenu;
import android.widget.SearchView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -24,6 +25,8 @@ import androidx.navigation.Navigation;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.FragmentArtistCatalogueBinding;
import com.cappielloantonio.tempo.helper.recyclerview.GridItemDecoration;
@@ -32,6 +35,10 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.viewmodel.ArtistCatalogueViewModel;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import java.util.ArrayList;
import java.util.List;
@UnstableApi
public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
@@ -125,23 +132,50 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setImeOptions(EditorInfo.IME_ACTION_DONE);
searchView.setQueryHint(getString(R.string.filter_artist));
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
searchView.clearFocus();
return false;
// this toast may be overkill...
Toast.makeText(requireContext(), "Search: " + query, Toast.LENGTH_SHORT).show();
filterArtists(query);
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
artistAdapter.getFilter().filter(newText);
return false;
filterArtists(newText);
return true;
}
});
searchView.setPadding(-32, 0, 0, 0);
}
private void filterArtists(String query) {
List<ArtistID3> allArtists = artistCatalogueViewModel.getArtistList().getValue();
if (allArtists == null || allArtists.isEmpty()) {
return;
}
if (query == null || query.trim().isEmpty()) {
artistAdapter.setItems(allArtists);
} else {
String searchQuery = query.toLowerCase().trim();
List<ArtistID3> filteredArtists = new ArrayList<>();
for (ArtistID3 artist : allArtists) {
if (artist.getName() != null &&
artist.getName().toLowerCase().contains(searchQuery)) {
filteredArtists.add(artist);
}
}
artistAdapter.setItems(filteredArtists);
}
}
private void hideKeyboard(View view) {
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);

View File

@@ -39,6 +39,7 @@ import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.Share;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
import com.cappielloantonio.tempo.ui.adapter.AlbumHorizontalAdapter;
@@ -111,6 +112,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
super.onViewCreated(view, savedInstanceState);
initSyncStarredView();
initSyncStarredAlbumsView();
initDiscoverSongSlideView();
initSimilarSongView();
initArtistRadio();
@@ -314,6 +316,63 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
});
}
private void initSyncStarredAlbumsView() {
if (Preferences.isStarredAlbumsSyncEnabled()) {
homeViewModel.getStarredAlbums(getViewLifecycleOwner()).observeForever(new Observer<List<AlbumID3>>() {
@Override
public void onChanged(List<AlbumID3> albums) {
if (albums != null) {
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
List<String> albumsToSync = new ArrayList<>();
int albumCount = 0;
for (AlbumID3 album : albums) {
boolean needsSync = false;
albumCount++;
albumsToSync.add(album.getName());
}
if (albumCount > 0) {
bind.homeSyncStarredAlbumsCard.setVisibility(View.VISIBLE);
String message = getResources().getQuantityString(
R.plurals.home_sync_starred_albums_count,
albumCount,
albumCount
);
bind.homeSyncStarredAlbumsToSync.setText(message);
}
}
homeViewModel.getStarredAlbums(getViewLifecycleOwner()).removeObserver(this);
}
});
}
bind.homeSyncStarredAlbumsCancel.setOnClickListener(v -> {
bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE);
});
bind.homeSyncStarredAlbumsDownload.setOnClickListener(v -> {
homeViewModel.getAllStarredAlbumSongs().observeForever(new Observer<List<Child>>() {
@Override
public void onChanged(List<Child> allSongs) {
if (allSongs != null) {
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
for (Child song : allSongs) {
if (!manager.isDownloaded(song.getId())) {
manager.download(MappingUtil.mapDownload(song), new Download(song));
}
}
}
homeViewModel.getAllStarredAlbumSongs().removeObserver(this);
bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE);
}
});
});
}
private void initDiscoverSongSlideView() {
if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_DISCOVERY)) return;

View File

@@ -8,6 +8,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.ToggleButton;
import android.widget.RatingBar;
@@ -66,6 +67,7 @@ public class PlayerControllerFragment extends Fragment {
private ConstraintLayout playerQuickActionView;
private ImageButton playerOpenQueueButton;
private ImageButton playerTrackInfo;
private LinearLayout ratingContainer;
private MainActivity activity;
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
@@ -123,6 +125,8 @@ public class PlayerControllerFragment extends Fragment {
playerOpenQueueButton = bind.getRoot().findViewById(R.id.player_open_queue_button);
playerTrackInfo = bind.getRoot().findViewById(R.id.player_info_track);
songRatingBar = bind.getRoot().findViewById(R.id.song_rating_bar);
ratingContainer = bind.getRoot().findViewById(R.id.rating_container);
checkAndSetRatingContainerVisibility();
}
private void initQuickActionView() {
@@ -430,6 +434,17 @@ public class PlayerControllerFragment extends Fragment {
playerMediaCoverViewPager.setCurrentItem(1, true);
}
private void checkAndSetRatingContainerVisibility() {
if (ratingContainer == null) return;
if (Preferences.showItemStarRating()) {
ratingContainer.setVisibility(View.VISIBLE);
}
else {
ratingContainer.setVisibility(View.GONE);
}
}
private void setPlaybackParameters(MediaBrowser mediaBrowser) {
Button playbackSpeedButton = bind.getRoot().findViewById(R.id.player_playback_speed_button);
float currentSpeed = Preferences.getPlaybackSpeed();

View File

@@ -9,6 +9,7 @@ import android.transition.TransitionManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
@@ -31,6 +32,7 @@ import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.google.android.material.snackbar.Snackbar;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@@ -120,8 +122,10 @@ public class PlayerCoverFragment extends Fragment {
});
bind.innerButtonTopRight.setOnClickListener(view -> {
ArrayList<Child> tracks = new ArrayList<>();
tracks.add(song);
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.TRACK_OBJECT, song);
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, tracks);
PlaylistChooserDialog dialog = new PlaylistChooserDialog();
dialog.setArguments(bundle);

View File

@@ -21,6 +21,7 @@ import androidx.media3.common.util.UnstableApi;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreference;
import com.cappielloantonio.tempo.BuildConfig;
import com.cappielloantonio.tempo.R;
@@ -31,6 +32,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 +96,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
actionLogout();
actionScan();
actionSyncStarredAlbums();
actionSyncStarredTracks();
actionChangeStreamingCacheStorage();
actionChangeDownloadStorage();
@@ -255,14 +258,30 @@ public class SettingsFragment extends PreferenceFragmentCompat {
findPreference("sync_starred_tracks_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
if (newValue instanceof Boolean) {
if ((Boolean) newValue) {
StarredSyncDialog dialog = new StarredSyncDialog();
StarredSyncDialog dialog = new StarredSyncDialog(() -> {
((SwitchPreference)preference).setChecked(false);
});
dialog.show(activity.getSupportFragmentManager(), null);
}
}
return true;
});
}
private void actionSyncStarredAlbums() {
findPreference("sync_starred_albums_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
if (newValue instanceof Boolean) {
if ((Boolean) newValue) {
StarredAlbumSyncDialog dialog = new StarredAlbumSyncDialog(() -> {
((SwitchPreference)preference).setChecked(false);
});
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"
@@ -63,6 +64,7 @@ object Preferences {
private const val ALWAYS_ON_DISPLAY = "always_on_display"
private const val AUDIO_QUALITY_PER_ITEM = "audio_quality_per_item"
private const val HOME_SECTOR_LIST = "home_sector_list"
private const val SONG_RATING_PER_ITEM = "song_rating_per_item"
private const val RATING_PER_ITEM = "rating_per_item"
private const val NEXT_UPDATE_CHECK = "next_update_check"
private const val CONTINUOUS_PLAY = "continuous_play"
@@ -300,6 +302,18 @@ object Preferences {
.apply()
}
@JvmStatic
fun isStarredAlbumsSyncEnabled(): Boolean {
return App.getInstance().preferences.getBoolean(SYNC_STARRED_ALBUMS_FOR_OFFLINE_USE, false)
}
@JvmStatic
fun setStarredAlbumsSyncEnabled(isStarredSyncEnabled: Boolean) {
App.getInstance().preferences.edit().putBoolean(
SYNC_STARRED_ALBUMS_FOR_OFFLINE_USE, isStarredSyncEnabled
).apply()
}
@JvmStatic
fun isStarredSyncEnabled(): Boolean {
return App.getInstance().preferences.getBoolean(SYNC_STARRED_TRACKS_FOR_OFFLINE_USE, false)
@@ -486,6 +500,11 @@ object Preferences {
App.getInstance().preferences.edit().putString(HOME_SECTOR_LIST, Gson().toJson(extension)).apply()
}
@JvmStatic
fun showItemStarRating(): Boolean {
return App.getInstance().preferences.getBoolean(SONG_RATING_PER_ITEM, false)
}
@JvmStatic
fun showItemRating(): Boolean {
return App.getInstance().preferences.getBoolean(RATING_PER_ITEM, false)

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

@@ -47,6 +47,8 @@ public class HomeViewModel extends AndroidViewModel {
private final PlaylistRepository playlistRepository;
private final SharingRepository sharingRepository;
private final StarredAlbumsSyncViewModel albumsSyncViewModel;
private final MutableLiveData<List<Child>> dicoverSongSample = new MutableLiveData<>(null);
private final MutableLiveData<List<AlbumID3>> newReleasedAlbum = new MutableLiveData<>(null);
private final MutableLiveData<List<Child>> starredTracksSample = new MutableLiveData<>(null);
@@ -82,6 +84,8 @@ public class HomeViewModel extends AndroidViewModel {
playlistRepository = new PlaylistRepository();
sharingRepository = new SharingRepository();
albumsSyncViewModel = new StarredAlbumsSyncViewModel(application);
setOfflineFavorite();
}
@@ -166,6 +170,10 @@ public class HomeViewModel extends AndroidViewModel {
return starredAlbums;
}
public LiveData<List<Child>> getAllStarredAlbumSongs() {
return albumsSyncViewModel.getAllStarredAlbumSongs();
}
public LiveData<List<ArtistID3>> getStarredArtists(LifecycleOwner owner) {
if (starredArtists.getValue() == null) {
artistRepository.getStarredArtists(true, 20).observe(owner, starredArtists::postValue);

View File

@@ -21,7 +21,7 @@ public class PlaylistChooserViewModel extends AndroidViewModel {
private final PlaylistRepository playlistRepository;
private final MutableLiveData<List<Playlist>> playlists = new MutableLiveData<>(null);
private ArrayList<Child> toAdd;
private ArrayList<Child> toAdd = new ArrayList<>();
public PlaylistChooserViewModel(@NonNull Application application) {
super(application);

View File

@@ -0,0 +1,90 @@
package com.cappielloantonio.tempo.viewmodel;
import android.app.Application;
import android.app.Activity;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.repository.AlbumRepository;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.Child;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class StarredAlbumsSyncViewModel extends AndroidViewModel {
private final AlbumRepository albumRepository;
private final MutableLiveData<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>> getAllStarredAlbumSongs() {
albumRepository.getStarredAlbums(false, -1).observeForever(new Observer<List<AlbumID3>>() {
@Override
public void onChanged(List<AlbumID3> albums) {
if (albums != null && !albums.isEmpty()) {
collectAllAlbumSongs(albums, starredAlbumSongs::postValue);
} else {
starredAlbumSongs.postValue(new ArrayList<>());
}
albumRepository.getStarredAlbums(false, -1).removeObserver(this);
}
});
return starredAlbumSongs;
}
public LiveData<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) {
LiveData<List<Child>> albumTracks = albumRepository.getAlbumTracks(album.getId());
albumTracks.observeForever(new Observer<List<Child>>() {
@Override
public void onChanged(List<Child> songs) {
if (songs != null) {
allSongs.addAll(songs);
}
latch.countDown();
if (latch.getCount() == 0) {
callback.onSongsCollected(allSongs);
albumTracks.removeObserver(this);
}
}
});
}
}
private interface AlbumSongsCallback {
void onSongsCollected(List<Child> songs);
}
}

View File

@@ -75,6 +75,39 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/rating_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:orientation="horizontal"
android:gravity="center"
android:scaleX="0.8"
android:scaleY="0.8"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/vertical_guideline"
app:layout_constraintTop_toBottomOf="@+id/player_media_quality_sector">
<RatingBar
android:id="@+id/song_rating_bar"
style="?android:attr/ratingBarStyleIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:numStars="5"
android:stepSize="1"
android:rating="0"
android:isIndicator="false" />
<TextView
android:id="@+id/rating_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textSize="12sp"
android:textColor="?attr/colorOnSurfaceVariant"
android:text=""/>
</LinearLayout>
<TextView
android:id="@+id/player_media_title_label"
style="@style/HeadlineLarge"

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

@@ -49,8 +49,9 @@
<TextView
android:id="@+id/subtitle_empty_description_label"
style="@style/LabelSmall"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:paddingStart="56dp"
android:paddingEnd="56dp"
android:text="@string/download_info_empty_subtitle" />

View File

@@ -106,6 +106,98 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Download/Sync starred albums -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/home_sync_starred_albums_card"
style="?attr/materialCardViewOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="24dp"
android:visibility="gone">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="20dp"
android:paddingVertical="12dp">
<!-- Title, secondary and supporting text -->
<TextView
android:id="@+id/home_sync_starred_albums_title"
style="@style/TitleLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/home_sync_starred_albums_title"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textFontWeight="600"
app:layout_constraintEnd_toStartOf="@id/vertical_guideline_albums"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/home_sync_starred_albums_subtitle"
style="@style/TitleMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/home_sync_starred_albums_subtitle"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_sync_starred_albums_title" />
<TextView
android:id="@+id/home_sync_starred_albums_to_sync"
style="@style/TitleSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:text="@string/home_sync_starred_albums_subtitle"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_sync_starred_albums_subtitle" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="end"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_sync_starred_albums_to_sync">
<com.google.android.material.button.MaterialButton
android:id="@+id/home_sync_starred_albums_cancel"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/home_sync_starred_cancel" />
<com.google.android.material.button.MaterialButton
android:id="@+id/home_sync_starred_albums_download"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/home_sync_starred_download" />
</LinearLayout>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/vertical_guideline_albums"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.90" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Discover music -->
<LinearLayout
android:id="@+id/home_discover_sector"

View File

@@ -11,4 +11,9 @@
android:icon="@drawable/ic_add"
android:title="@string/menu_add_to_playlist_button"
app:showAsAction="never" />
<item
android:id="@+id/action_rate_album"
android:icon="@drawable/ic_add"
android:title="@string/menu_rate_album"
app:showAsAction="never" />
</menu>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:title="@string/search_title_artist"
android:icon="@drawable/ic_search"
app:showAsAction="ifRoom|collapseActionView"
app:actionViewClass="androidx.appcompat.widget.SearchView" />
</menu>

View File

@@ -6,6 +6,7 @@
<string name="activity_battery_optimizations_summary">Por favor, desactive las optimizaciones de batería para continuar la reproducción multimedia mientras la pantalla está apagada.</string>
<string name="activity_battery_optimizations_title">Optimizaciones de batería</string>
<string name="activity_info_offline_mode">Modo sin conexión</string>
<string name="album_bottom_sheet_add_to_playlist">Añadir a la lista de reproducción</string>
<string name="album_bottom_sheet_add_to_queue">Añadir a la cola</string>
<string name="album_bottom_sheet_download_all">Descargar todo</string>
<string name="album_bottom_sheet_go_to_artist">Ir al artista</string>
@@ -68,7 +69,7 @@
<string name="download_directory_dialog_positive_button">Descargar</string>
<string name="download_directory_dialog_summary">Se descargarán todas las pistas de esta carpeta. Las pistas en las subcarpetas no se descargarán.</string>
<string name="download_directory_dialog_title">Descargar las pistas</string>
<string name="download_info_empty_subtitle">Una vez que descargues un tema, lo encontrarás aquí</string>
<string name="download_info_empty_subtitle">Una vez que descargues una pista, la encontrarás aquí</string>
<string name="download_info_empty_title">No hay descargas</string>
<string name="download_item_multiple_subtitle_formatter">%1$s • %2$s elementos</string>
<string name="download_item_single_subtitle_formatter">%1$s elementos</string>
@@ -89,6 +90,7 @@
<string name="exo_download_notification_channel_name">Descargas</string>
<string name="filter_info_selection">Selecciona dos o más filtros</string>
<string name="filter_title">Filtrar</string>
<string name="filter_artist">Filtrar artistas</string>
<string name="filter_title_expanded">Filtrar géneros</string>
<string name="generic_list_page_count">(%1$d)</string>
<string name="generic_list_page_count_unknown">(+%1$d)</string>
@@ -115,6 +117,7 @@
<string name="home_sync_starred_download">Descargar</string>
<string name="home_sync_starred_subtitle">Descargar estas pistas usará una gran cantidad de datos</string>
<string name="home_sync_starred_title">Parece que hay algunas pistas destacadas para sincronizar</string>
<string name="home_sync_starred_albums_subtitle">Los álbumes marcados como favoritos estarán disponibles en el modo sin conexión.</string>
<string name="home_title_best_of">Lo mejor de</string>
<string name="home_title_discovery">Descubrir</string>
<string name="home_title_discovery_shuffle_all_button">Todo en aleatorio</string>
@@ -159,7 +162,9 @@
<string name="login_title_expanded">Servidores de Subsonic</string>
<string name="media_route_menu_title">Emitir</string>
<string name="menu_add_button">Añadir</string>
<string name="menu_add_to_playlist_button">Añadir a la lista de reproducción</string>
<string name="menu_download_all_button">Descargar todo</string>
<string name="menu_rate_album">Valorar álbum</string>
<string name="menu_download_label">Descargas</string>
<string name="menu_filter_all">Todo</string>
<string name="menu_filter_download">Descargado</string>
@@ -195,6 +200,7 @@
<string name="menu_sort_year">Año</string>
<string name="player_playback_speed">%1$.2fx</string>
<string name="player_queue_clean_all_button">Limpiar la cola de reproducción</string>
<string name="player_queue_save_queue_success">Cola de reproducción guardada</string>
<string name="player_server_priority">Prioridad del servidor</string>
<string name="player_unknown_format">Formato desconocido</string>
<string name="player_transcoding">Transcodificando</string>
@@ -205,6 +211,7 @@
<string name="playlist_chooser_dialog_negative_button">Cancelar</string>
<string name="playlist_chooser_dialog_neutral_button">Crear</string>
<string name="playlist_chooser_dialog_title">Añadir a una lista de reproducción</string>
<string name="playlist_chooser_dialog_toast_add_failure">Error al añadir a la lista</string>
<string name="playlist_counted_tracks">%1$d pistas • %2$s</string>
<string name="playlist_duration">Duración • %1$s</string>
<string name="playlist_editor_dialog_action_delete_toast">Pulsación larga para eliminar</string>
@@ -247,7 +254,7 @@
<string name="rating_dialog_negative_button">Cancelar</string>
<string name="rating_dialog_positive_button">Guardar</string>
<string name="rating_dialog_title">Valorar</string>
<string name="search_hint">Buscar tema, artistas o álbumes</string>
<string name="search_hint">Buscar pista, artistas o álbumes</string>
<string name="search_info_minimum_characters">Introduzca al menos tres caracteres</string>
<string name="search_title_album">Álbumes</string>
<string name="settings_equalizer_summary">Ajustes de audio</string>
@@ -306,6 +313,7 @@
<string name="settings_podcast_summary">Si está habilitada, se mostrará la sección de pódcasts. Reinicia la aplicación para que los cambios surtan efecto.</string>
<string name="settings_audio_quality">Mostrar calidad de audio</string>
<string name="settings_audio_quality_summary">La tasa de bits y el formato de audio se mostrarán para cada pista de audio.</string>
<string name="settings_song_rating_summary">Si está habilitada, muestra la valoración de la pista como barra de 5 estrellas en la página del control de reproducción.\n\n*Requiere reiniciar la aplicación</string>
<string name="settings_item_rating">Mostrar valoración de los elementos</string>
<string name="settings_queue_syncing_title">Sincronizar cola de reproducción para este usuario</string>
<string name="settings_radio">Mostrar emisoras de radio</string>
@@ -314,6 +322,7 @@
<string name="settings_rounded_corner_size">Tamaño de las esquinas</string>
<string name="settings_rounded_corner_summary">Si está habilitada, establece un ángulo de curvatura para todas las portadas de álbumes. Los cambios se aplicarán después de reiniciar la app.</string>
<string name="settings_scan_title">Escanear biblioteca</string>
<string name="streaming_cache_storage_dialog_summary">Cambiar la ubicación de los archivos en caché a otro almacenamiento puede causar el borrado de todos los archivos en caché en el anterior almacenamiento.</string>
<string name="streaming_cache_storage_dialog_title">Seleccióna un tipo de almacenamiento</string>
<string name="streaming_cache_storage_external_dialog_positive_button">Externo</string>
<string name="streaming_cache_storage_internal_dialog_negative_button">Interno</string>
@@ -410,13 +419,20 @@
<string name="settings_summary_transcoding">La prioridad que se aplica al modo de transcodificación. Si se selecciona \"Reproducción directa\", la tasa de bits del archivo no cambiará.</string>
<string name="starred_sync_dialog_summary">Descargar las pistas destacadas podría consumir una gran cantidad de datos</string>
<string name="starred_sync_dialog_title">Sincronizar las pistas destacadas</string>
<string name="starred_album_sync_dialog_title">Sincronizar álbumes favoritos</string>
<string name="streaming_cache_storage_dialog_sub_summary">Para que los cambios tengan efecto, reinicia la app.</string>
<string name="settings_summary_transcoding_download">Descarga los archivos multimedia transcodificados. Si esta opción está habilitada, no se usará el endpoint de descarga, sino las siguientes opciones.\n\nSi el formato de transcodificación para las descargas se establece en \"Descarga directa\", no se modificará la tasa de bits del archivo.</string>
<string name="settings_summary_transcoding_estimate_content_length">Cuando el archivo se transcodifica en tiempo real, el cliente normalmente no muestra la duración de la pista. Es posible solicitar a los servidores que soporten esta característica, que calculen la duración de la pista que se está reproduciendo, pero los tiempos de respuesta podrían aumentar.</string>
<string name="settings_sync_starred_albums_for_offline_use_title">Sincronizar álbumes favoritos para uso sin conexión</string>
<string name="settings_sync_starred_tracks_for_offline_use_summary">Si está habilitada, las pistas destacadas se descargarán para uso sin conexión.</string>
<string name="track_info_summary_downloaded_file">El archivo se ha descargado usando las APIs de Subsonic. El códec y la tasa de bits del archivo se mantienen sin cambios respecto al archivo de origen.</string>
<string name="track_info_summary_full_transcode">La aplicación pedirá al servidor transcodificar el archivo y modificar su tasa de bits. El códec pedido por el usuario es %1$s, con una tasa de bits de %2$s. Cualquier cambio en el códec y tasa de bits del archivo en el formato elegido será manejado por el servidor, que puede, o no, soportar esta operación.</string>
<string name="track_info_summary_original_file">La aplicación solo leerá el archivo original ofrecido por el servidor. La app pedirá al servidor, de forma explícita, el archivo sin transcodificar con la tasa de bits de origen.</string>
<string name="track_info_summary_server_prioritized">La calidad del archivo a reproducir queda a decisión del servidor. La app no forzará la elección del códec y la tasa de bits para ninguna posible transcodificación.</string>
<string name="track_info_summary_transcoding_bitrate">La aplicación pedirá al servidor modificar la tasa de bits del archivo. El usuario ha pedido una tasa de bits de %1$s, mientras que el códec del archivo origen se mantendrá sin cambios. Cualquier cambio en la tasa de bits del archivo en el formato seleccionado será realizado por el servidor, que podrá soportar, o no, la operación.</string>
<string name="playlist_chooser_dialog_toast_add_success">Se ha añadido a la lista</string>
<string name="settings_song_rating">Mostrar valoración de las pistas</string>
<string name="home_sync_starred_albums_title">Sincronizar álbumes favoritos</string>
<string name="settings_sync_starred_albums_for_offline_use_summary">Si está habilitada, los álbumes favoritos se descargarán para uso sin conexión.</string>
<string name="starred_album_sync_dialog_summary">Descargar los álbumes favoritos puede consumir una gran cantidad de datos.</string>
</resources>

View File

@@ -90,6 +90,7 @@
<string name="exo_download_notification_channel_name">Téléchargements</string>
<string name="filter_info_selection">Sélectionnez deux filtres ou plus</string>
<string name="filter_title">Filtrer</string>
<string name="filter_artist">Filtrer par artiste</string>
<string name="filter_title_expanded">Filtrer par genre</string>
<string name="generic_list_page_count">(%1$d)</string>
<string name="generic_list_page_count_unknown">(+%1$d)</string>
@@ -162,6 +163,7 @@
<string name="menu_add_button">Ajouter</string>
<string name="menu_add_to_playlist_button">Ajouter à une playlist</string>
<string name="menu_download_all_button">Télécharger tout</string>
<string name="menu_rate_album">Noter l\'album</string>
<string name="menu_download_label">Téléchargé</string>
<string name="menu_filter_all">Tout</string>
<string name="menu_filter_download">Téléchargé</string>
@@ -190,7 +192,11 @@
<string name="menu_sort_year">Année</string>
<string name="player_playback_speed">%1$.2fx</string>
<string name="player_queue_clean_all_button">Vider la file d\'attente</string>
<string name="player_queue_save_queue_success">File d\'attente sauvegardée</string>
<string name="player_server_priority">Priorité serveur</string>
<string name="player_unknown_format">Format inconnu</string>
<string name="player_transcoding">Transcodage</string>
<string name="player_transcoding_requested">demandé</string>
<string name="playlist_catalogue_title">Catalogue des Playlists</string>
<string name="playlist_catalogue_title_expanded">Parcourir les playlists</string>
<string name="playlist_chooser_dialog_empty">Pas de playlist</string>
@@ -299,13 +305,15 @@
<string name="settings_max_bitrate_download">Débit binaire pour les téléchargements</string>
<string name="settings_max_bitrate_mobile">Débit binaire en données mobile</string>
<string name="settings_max_bitrate_wifi">Débit binaire en Wi-Fi</string>
<string name="settings_media_cache">Taille du cache des fichiers audios</string>
<string name="settings_media_cache">Taille du cache des fichiers audio</string>
<string name="settings_music_directory">Afficher les dossiers</string>
<string name="settings_music_directory_summary">Si activé, rend possible la navigation dans les répertoires. À noter que pour que la navigation dans les dossiers fonctionne correctement, le serveur doit supporter cette fonctionnalité.</string>
<string name="settings_podcast">Voir les podcasts</string>
<string name="settings_podcast_summary">Si activé, rend visible la section Podcast</string>
<string name="settings_podcast_summary">Si activé, rend visible la section Podcast. Redémarrez l\'application pour appliquer ce paramètre.</string>
<string name="settings_audio_quality">Afficher la qualité audio</string>
<string name="settings_audio_quality_summary">Le débit binaire et le format audio seront affichés pour chaque piste.</string>
<string name="settings_song_rating">Afficher la note de la piste</string>
<string name="settings_song_rating_summary">Si activé, rend visible la note de la piste sur sa page\n\n*Nécessite le redémarrage de l\'application</string>
<string name="settings_item_rating">Afficher la note</string>
<string name="settings_item_rating_summary">Si activé, la note et le statut de mise en favori de l\'élément seront affichés.</string>
<string name="settings_queue_syncing_countdown">Minuteur de synchronisation</string>

View File

@@ -90,6 +90,7 @@
<string name="exo_download_notification_channel_name">Downloads</string>
<string name="filter_info_selection">Select two or more filters</string>
<string name="filter_title">Filter</string>
<string name="filter_artist">Filter artists</string>
<string name="filter_title_expanded">Filter Genres</string>
<string name="generic_list_page_count">(%1$d)</string>
<string name="generic_list_page_count_unknown">(+%1$d)</string>
@@ -116,6 +117,8 @@
<string name="home_sync_starred_download">Download</string>
<string name="home_sync_starred_subtitle">Downloading these tracks may involve significant data usage</string>
<string name="home_sync_starred_title">Looks like there are some starred tracks to sync</string>
<string name="home_sync_starred_albums_title">Sync Starred Albums</string>
<string name="home_sync_starred_albums_subtitle">Albums marked with a star will be available offline</string>
<string name="home_title_best_of">Best of</string>
<string name="home_title_discovery">Discovery</string>
<string name="home_title_discovery_shuffle_all_button">Shuffle all</string>
@@ -164,6 +167,7 @@
<string name="menu_add_button">Add</string>
<string name="menu_add_to_playlist_button">Add to playlist</string>
<string name="menu_download_all_button">Download all</string>
<string name="menu_rate_album">Rate album</string>
<string name="menu_download_label">Download</string>
<string name="menu_filter_all">All</string>
<string name="menu_filter_download">Downloaded</string>
@@ -312,6 +316,8 @@
<string name="settings_podcast_summary">If enabled, show the podcast section. Restart the app for it to take full effect.</string>
<string name="settings_audio_quality">Show audio quality</string>
<string name="settings_audio_quality_summary">The bitrate and audio format will be shown for each audio track.</string>
<string name="settings_song_rating">Show song star rating</string>
<string name="settings_song_rating_summary">If enabled, shows 5 star rating for track on song page\n\n*Requires App restart</string>
<string name="settings_item_rating">Show item rating</string>
<string name="settings_item_rating_summary">If enabled, the item\'s rating and whether it is marked as a favorite will be displayed.</string>
<string name="settings_queue_syncing_countdown">Sync timer</string>
@@ -340,6 +346,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>
@@ -393,8 +401,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>
@@ -429,4 +439,8 @@
<string name="undraw_page">unDraw</string>
<string name="undraw_thanks">A special thanks goes to unDraw without whose illustrations we could not have made this application more beautiful.</string>
<string name="undraw_url">https://undraw.co/</string>
<plurals name="home_sync_starred_albums_count">
<item quantity="one">%d album to sync</item>
<item quantity="other">%d albums to sync</item>
</plurals>
</resources>

View File

@@ -57,6 +57,12 @@
android:summary="@string/settings_audio_quality_summary"
android:key="audio_quality_per_item" />
<SwitchPreference
android:title="@string/settings_song_rating"
android:defaultValue="false"
android:summary="@string/settings_song_rating_summary"
android:key="song_rating_per_item" />
<SwitchPreference
android:title="@string/settings_item_rating"
android:defaultValue="false"
@@ -133,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"

BIN
mockup/usage/fave_album.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB