Merge branch 'nitehu-feature/fivestarrating' into develop

This commit is contained in:
Óscar García Amor 2020-03-23 09:50:53 +01:00
commit 8a798ac456
32 changed files with 765 additions and 128 deletions

View File

@ -63,7 +63,9 @@ class MusicDirectory {
var type: String? = null,
var created: Date? = null,
var closeness: Int = 0,
var bookmarkPosition: Int = 0
var bookmarkPosition: Int = 0,
var userRating: Int? = null,
var averageRating: Float? = null
) : Serializable {
fun setDuration(duration: Long) {
this.duration = duration.toInt()

View File

@ -0,0 +1,55 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should be`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
/**
* Integration test for [SubsonicAPIClient] for setRating request.
*/
class SubsonicApiSetRatingTest : SubsonicAPIClientTest() {
@Test
fun `Should parse setRating ok response`() {
val id = "110"
val rating = 3
mockWebServerRule.enqueueResponse("ping_ok.json")
val response = client.api.setRating(id, rating).execute()
assertResponseSuccessful(response)
response.body()?.status `should be` SubsonicResponse.Status.OK
}
@Test
fun `Should parse setRating error response`() {
val id = "110223"
val rating = 5
checkErrorCallParsed(mockWebServerRule) {
client.api.setRating(id, rating).execute()
}
}
@Test
fun `Should pass id parameter`() {
val id = "110"
val rating = 5
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "id=$id") {
client.api.setRating(id, rating).execute()
}
}
@Test
fun `Should pass rating parameter`() {
val id = "110"
val rating = 5
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "rating=$rating") {
client.api.setRating(id, rating).execute()
}
}
}

View File

@ -79,6 +79,12 @@ interface SubsonicAPIDefinition {
@Query("artistId") artistId: String? = null
): Call<SubsonicResponse>
@GET("setRating.view")
fun setRating(
@Query("id") id: String,
@Query("rating") rating: Int
): Call<SubsonicResponse>
@GET("getArtist.view")
fun getArtist(@Query("id") id: String): Call<GetArtistResponse>

View File

@ -50,11 +50,15 @@ import android.widget.TextView;
import android.widget.ViewFlipper;
import com.mobeta.android.dslv.DragSortListView;
import org.koin.java.standalone.KoinJavaComponent;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.domain.RepeatMode;
import org.moire.ultrasonic.featureflags.Feature;
import org.moire.ultrasonic.featureflags.FeatureStorage;
import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.DownloadService;
import org.moire.ultrasonic.service.MusicService;
@ -119,6 +123,15 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
private SilentBackgroundTask<Void> onProgressChangedTask;
LinearLayout visualizerViewLayout;
private MenuItem starMenuItem;
private LinearLayout ratingLinearLayout;
private ImageView fiveStar1ImageView;
private ImageView fiveStar2ImageView;
private ImageView fiveStar3ImageView;
private ImageView fiveStar4ImageView;
private ImageView fiveStar5ImageView;
private boolean useFiveStarRating;
private Drawable hollowStar;
private Drawable fullStar;
/**
* Called when the activity is first created.
@ -136,6 +149,8 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
int width = size.x;
int height = size.y;
useFiveStarRating = KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING);
swipeDistance = (width + height) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100;
swipeVelocity = swipeDistance;
gestureScanner = new GestureDetector(this, this);
@ -162,6 +177,54 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
visualizerViewLayout = (LinearLayout) findViewById(R.id.download_visualizer_view_layout);
ratingLinearLayout = findViewById(R.id.song_rating);
fiveStar1ImageView = findViewById(R.id.song_five_star_1);
fiveStar2ImageView = findViewById(R.id.song_five_star_2);
fiveStar3ImageView = findViewById(R.id.song_five_star_3);
fiveStar4ImageView = findViewById(R.id.song_five_star_4);
fiveStar5ImageView = findViewById(R.id.song_five_star_5);
if (!useFiveStarRating) ratingLinearLayout.setVisibility(View.GONE);
hollowStar = Util.getDrawableFromAttribute(SubsonicTabActivity.getInstance(), R.attr.star_hollow);
fullStar = Util.getDrawableFromAttribute(SubsonicTabActivity.getInstance(), R.attr.star_full);
fiveStar1ImageView.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(final View view) {
setSongRating(1);
}
});
fiveStar2ImageView.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(final View view) {
setSongRating(2);
}
});
fiveStar3ImageView.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(final View view) {
setSongRating(3);
}
});
fiveStar4ImageView.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(final View view) {
setSongRating(4);
}
});
fiveStar5ImageView.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(final View view) {
setSongRating(5);
}
});
View.OnTouchListener touchListener = new View.OnTouchListener()
{
@Override
@ -726,22 +789,20 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
currentSong = downloadFile.getSong();
}
if (useFiveStarRating) starMenuItem.setVisible(false);
if (currentSong != null)
{
final Drawable starDrawable = currentSong.getStarred() ? Util.getDrawableFromAttribute(SubsonicTabActivity.getInstance(), R.attr.star_full) : Util.getDrawableFromAttribute(SubsonicTabActivity.getInstance(), R.attr.star_hollow);
if (starMenuItem != null)
{
starMenuItem.setIcon(starDrawable);
starMenuItem.setIcon(currentSong.getStarred() ? fullStar : hollowStar);
}
}
else
{
final Drawable starDrawable = Util.getDrawableFromAttribute(SubsonicTabActivity.getInstance(), R.attr.star_hollow);
if (starMenuItem != null)
{
starMenuItem.setIcon(starDrawable);
starMenuItem.setIcon(hollowStar);
}
}
@ -974,12 +1035,12 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
if (isStarred)
{
starMenuItem.setIcon(Util.getDrawableFromAttribute(SubsonicTabActivity.getInstance(), R.attr.star_hollow));
starMenuItem.setIcon(hollowStar);
currentSong.setStarred(false);
}
else
{
starMenuItem.setIcon(Util.getDrawableFromAttribute(SubsonicTabActivity.getInstance(), R.attr.star_full));
starMenuItem.setIcon(fullStar);
currentSong.setStarred(true);
}
@ -1320,6 +1381,8 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
downloadTrackTextView.setText(trackFormat);
downloadTotalDurationTextView.setText(duration);
getImageLoader().loadImage(albumArtImageView, currentSong, true, 0, false, true);
displaySongRating();
}
else
{
@ -1440,6 +1503,9 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
break;
}
// TODO: It would be a lot nicer if DownloadService would send an event when this is necessary instead of updating every time
displaySongRating();
onProgressChangedTask = null;
}
};
@ -1580,4 +1646,23 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
{
return progressBar;
}
private void displaySongRating()
{
int rating = currentSong.getUserRating() == null ? 0 : currentSong.getUserRating();
fiveStar1ImageView.setImageDrawable(rating > 0 ? fullStar : hollowStar);
fiveStar2ImageView.setImageDrawable(rating > 1 ? fullStar : hollowStar);
fiveStar3ImageView.setImageDrawable(rating > 2 ? fullStar : hollowStar);
fiveStar4ImageView.setImageDrawable(rating > 3 ? fullStar : hollowStar);
fiveStar5ImageView.setImageDrawable(rating > 4 ? fullStar : hollowStar);
}
private void setSongRating(final int rating)
{
if (currentSong == null)
return;
displaySongRating();
getDownloadService().setSongRating(rating);
}
}

View File

@ -172,10 +172,11 @@ public class SettingsFragment extends PreferenceFragment
}
private void setupFeatureFlagsPreferences() {
final FeatureStorage featureStorage = KoinJavaComponent.get(FeatureStorage.class);
CheckBoxPreference ffImageLoader = (CheckBoxPreference) findPreference(
Constants.PREFERENCES_KEY_FF_IMAGE_LOADER);
final FeatureStorage featureStorage = KoinJavaComponent.get(FeatureStorage.class);
if (ffImageLoader != null) {
ffImageLoader.setChecked(featureStorage.isFeatureEnabled(Feature.NEW_IMAGE_DOWNLOADER));
ffImageLoader.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@ -187,6 +188,21 @@ public class SettingsFragment extends PreferenceFragment
}
});
}
CheckBoxPreference useFiveStarRating = (CheckBoxPreference) findPreference(
Constants.PREFERENCES_KEY_USE_FIVE_STAR_RATING);
if (useFiveStarRating != null) {
useFiveStarRating.setChecked(featureStorage.isFeatureEnabled(Feature.FIVE_STAR_RATING));
useFiveStarRating.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object o) {
featureStorage.changeFeatureFlag(Feature.FIVE_STAR_RATING, (Boolean) o);
return true;
}
});
}
}
private void setupGaplessControlSettingsV14() {

View File

@ -385,7 +385,6 @@ public class CachedMusicService implements MusicService
public void star(String id, String albumId, String artistId, Context context, ProgressListener progressListener) throws Exception
{
musicService.star(id, albumId, artistId, context, progressListener);
}
@Override
@ -394,6 +393,12 @@ public class CachedMusicService implements MusicService
musicService.unstar(id, albumId, artistId, context, progressListener);
}
@Override
public void setRating(String id, int rating, Context context, ProgressListener progressListener) throws Exception
{
musicService.setRating(id, rating, context, progressListener);
}
@Override
public List<Genre> getGenres(Context context, ProgressListener progressListener) throws Exception
{

View File

@ -144,4 +144,8 @@ public interface DownloadService
void stopJukeboxService();
void startJukeboxService();
void updateNotification();
void setSongRating(final int rating);
}

View File

@ -39,9 +39,11 @@ import android.os.PowerManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;
import android.widget.SeekBar;
import org.koin.java.standalone.KoinJavaComponent;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.DownloadActivity;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
@ -52,6 +54,8 @@ import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.domain.RepeatMode;
import org.moire.ultrasonic.domain.UserInfo;
import org.moire.ultrasonic.featureflags.Feature;
import org.moire.ultrasonic.featureflags.FeatureStorage;
import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x1;
import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x2;
import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x3;
@ -1238,11 +1242,7 @@ public class DownloadServiceImpl extends Service implements DownloadService
// Only update notification is player state is one that will change the icon
if (this.playerState == PlayerState.STARTED || this.playerState == PlayerState.PAUSED)
{
if (Util.isNotificationEnabled(this)) {
final NotificationManagerCompat notificationManager =
NotificationManagerCompat.from(this);
notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification());
}
updateNotification();
tabInstance.showNowPlaying();
}
}
@ -2071,6 +2071,16 @@ public class DownloadServiceImpl extends Service implements DownloadService
}
}
@Override
public void updateNotification()
{
if (Util.isNotificationEnabled(this)) {
final NotificationManagerCompat notificationManager =
NotificationManagerCompat.from(this);
notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification());
}
}
@SuppressWarnings("IconColors")
private Notification buildForegroundNotification() {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
@ -2103,6 +2113,7 @@ public class DownloadServiceImpl extends Service implements DownloadService
final String title = song.getTitle();
final String text = song.getArtist();
final String album = song.getAlbum();
final int rating = song.getUserRating() == null ? 0 : song.getUserRating();
final int imageSize = Util.getNotificationImageSize(this);
try {
@ -2127,6 +2138,17 @@ public class DownloadServiceImpl extends Service implements DownloadService
contentView.setTextViewText(R.id.album, album);
bigView.setTextViewText(R.id.album, album);
boolean useFiveStarRating = KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING);
if (!useFiveStarRating) bigView.setViewVisibility(R.id.notification_rating, View.INVISIBLE);
else
{
bigView.setImageViewResource(R.id.notification_five_star_1, rating > 0 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark);
bigView.setImageViewResource(R.id.notification_five_star_2, rating > 1 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark);
bigView.setImageViewResource(R.id.notification_five_star_3, rating > 2 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark);
bigView.setImageViewResource(R.id.notification_five_star_4, rating > 3 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark);
bigView.setImageViewResource(R.id.notification_five_star_5, rating > 4 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark);
}
Notification notification = builder.build();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
notification.bigContentView = bigView;
@ -2135,6 +2157,38 @@ public class DownloadServiceImpl extends Service implements DownloadService
return notification;
}
public void setSongRating(final int rating)
{
if (!KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING))
return;
if (currentPlaying == null)
return;
final Entry song = currentPlaying.getSong();
song.setUserRating(rating);
new Thread(new Runnable()
{
@Override
public void run()
{
final MusicService musicService = MusicServiceFactory.getMusicService(DownloadServiceImpl.this);
try
{
musicService.setRating(song.getId(), rating, DownloadServiceImpl.this, null);
}
catch (Exception e)
{
Log.e(TAG, e.getMessage(), e);
}
}
}).start();
updateNotification();
}
private class BufferTask extends CancellableTask
{
private final DownloadFile downloadFile;

View File

@ -329,6 +329,21 @@ public class DownloadServiceLifecycleSupport
case KeyEvent.KEYCODE_MEDIA_PAUSE:
downloadService.pause();
break;
case KeyEvent.KEYCODE_1:
downloadService.setSongRating(1);
break;
case KeyEvent.KEYCODE_2:
downloadService.setSongRating(2);
break;
case KeyEvent.KEYCODE_3:
downloadService.setSongRating(3);
break;
case KeyEvent.KEYCODE_4:
downloadService.setSongRating(4);
break;
case KeyEvent.KEYCODE_5:
downloadService.setSongRating(5);
break;
default:
break;
}

View File

@ -59,6 +59,8 @@ public interface MusicService
void unstar(String id, String albumId, String artistId, Context context, ProgressListener progressListener) throws Exception;
void setRating(String id, int rating, Context context, ProgressListener progressListener) throws Exception;
List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception;

View File

@ -238,6 +238,17 @@ public class RESTMusicService implements MusicService {
checkResponseSuccessful(response);
}
@Override
public void setRating(String id,
int rating,
Context context,
ProgressListener progressListener) throws Exception {
updateProgressListener(progressListener, R.string.parser_reading);
Response<SubsonicResponse> response = subsonicAPIClient.getApi()
.setRating(id, rating).execute();
checkResponseSuccessful(response);
}
@Override
public MusicDirectory getMusicDirectory(String id,
String name,

View File

@ -131,6 +131,7 @@ public final class Constants
public static final String PREFERENCES_KEY_SCAN_MEDIA = "scanMedia";
public static final String PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY = "imageLoaderConcurrency";
public static final String PREFERENCES_KEY_FF_IMAGE_LOADER = "ff_new_image_loader";
public static final String PREFERENCES_KEY_USE_FIVE_STAR_RATING = "use_five_star_rating";
// Number of free trial days for non-licensed servers.
public static final int FREE_TRIAL_DAYS = 30;

View File

@ -1306,6 +1306,36 @@ public class Util extends DownloadActivity
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_STOP));
pendingIntent = PendingIntent.getService(context, 0, intent, 0);
views.setOnClickPendingIntent(R.id.control_stop, pendingIntent);
intent = new Intent("RATE_1");
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_1));
pendingIntent = PendingIntent.getService(context, 0, intent, 0);
views.setOnClickPendingIntent(R.id.notification_five_star_1, pendingIntent);
intent = new Intent("RATE_2");
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_2));
pendingIntent = PendingIntent.getService(context, 0, intent, 0);
views.setOnClickPendingIntent(R.id.notification_five_star_2, pendingIntent);
intent = new Intent("RATE_3");
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_3));
pendingIntent = PendingIntent.getService(context, 0, intent, 0);
views.setOnClickPendingIntent(R.id.notification_five_star_3, pendingIntent);
intent = new Intent("RATE_4");
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_4));
pendingIntent = PendingIntent.getService(context, 0, intent, 0);
views.setOnClickPendingIntent(R.id.notification_five_star_4, pendingIntent);
intent = new Intent("RATE_5");
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_5));
pendingIntent = PendingIntent.getService(context, 0, intent, 0);
views.setOnClickPendingIntent(R.id.notification_five_star_5, pendingIntent);
}
public static int getNetworkTimeout(Context context)

View File

@ -23,6 +23,7 @@ import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.CheckedTextView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
@ -120,6 +121,12 @@ public class EntryAdapter extends ArrayAdapter<Entry>
TextView status;
TextView artist;
TextView duration;
LinearLayout rating;
ImageView fiveStar1;
ImageView fiveStar2;
ImageView fiveStar3;
ImageView fiveStar4;
ImageView fiveStar5;
ImageView star;
ImageView drag;
}

View File

@ -28,10 +28,14 @@ import android.view.View;
import android.widget.Checkable;
import android.widget.CheckedTextView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.koin.java.standalone.KoinJavaComponent;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.featureflags.Feature;
import org.moire.ultrasonic.featureflags.FeatureStorage;
import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.DownloadService;
import org.moire.ultrasonic.service.DownloadServiceImpl;
@ -73,12 +77,15 @@ public class SongView extends UpdateView implements Checkable
private boolean playing;
private EntryAdapter.SongViewHolder viewHolder;
private boolean maximized = false;
private boolean useFiveStarRating;
public SongView(Context context)
{
super(context);
this.context = context;
useFiveStarRating = KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING);
String theme = Util.getTheme(context);
boolean themesMatch = theme.equals(SongView.theme);
inflater = LayoutInflater.from(this.context);
@ -124,6 +131,12 @@ public class SongView extends UpdateView implements Checkable
inflater.inflate(song.isVideo() ? R.layout.video_list_item : R.layout.song_list_item, this, true);
viewHolder = new EntryAdapter.SongViewHolder();
viewHolder.check = (CheckedTextView) findViewById(R.id.song_check);
viewHolder.rating = (LinearLayout) findViewById(R.id.song_rating);
viewHolder.fiveStar1 = (ImageView) findViewById(R.id.song_five_star_1);
viewHolder.fiveStar2 = (ImageView) findViewById(R.id.song_five_star_2);
viewHolder.fiveStar3 = (ImageView) findViewById(R.id.song_five_star_3);
viewHolder.fiveStar4 = (ImageView) findViewById(R.id.song_five_star_4);
viewHolder.fiveStar5 = (ImageView) findViewById(R.id.song_five_star_5);
viewHolder.star = (ImageView) findViewById(R.id.song_star);
viewHolder.drag = (ImageView) findViewById(R.id.song_drag);
viewHolder.track = (TextView) findViewById(R.id.song_track);
@ -237,56 +250,59 @@ public class SongView extends UpdateView implements Checkable
if (Util.isOffline(this.context))
{
viewHolder.star.setVisibility(View.GONE);
viewHolder.rating.setVisibility(View.GONE);
}
else
{
viewHolder.star.setImageDrawable(song.getStarred() ? starDrawable : starHollowDrawable);
viewHolder.star.setOnClickListener(new View.OnClickListener()
if (useFiveStarRating)
{
@Override
public void onClick(View view)
{
final boolean isStarred = song.getStarred();
final String id = song.getId();
viewHolder.star.setVisibility(View.GONE);
if (!isStarred)
{
viewHolder.star.setImageDrawable(starDrawable);
song.setStarred(true);
}
else
{
viewHolder.star.setImageDrawable(starHollowDrawable);
song.setStarred(false);
}
int rating = song.getUserRating() == null ? 0 : song.getUserRating();
viewHolder.fiveStar1.setImageDrawable(rating > 0 ? starDrawable : starHollowDrawable);
viewHolder.fiveStar2.setImageDrawable(rating > 1 ? starDrawable : starHollowDrawable);
viewHolder.fiveStar3.setImageDrawable(rating > 2 ? starDrawable : starHollowDrawable);
viewHolder.fiveStar4.setImageDrawable(rating > 3 ? starDrawable : starHollowDrawable);
viewHolder.fiveStar5.setImageDrawable(rating > 4 ? starDrawable : starHollowDrawable);
new Thread(new Runnable()
{
@Override
public void run()
{
MusicService musicService = MusicServiceFactory.getMusicService(SongView.this.context);
}
else {
viewHolder.rating.setVisibility(View.GONE);
viewHolder.star.setImageDrawable(song.getStarred() ? starDrawable : starHollowDrawable);
try
{
if (!isStarred)
{
musicService.star(id, null, null, SongView.this.context, null);
}
else
{
musicService.unstar(id, null, null, SongView.this.context, null);
}
}
catch (Exception e)
{
Log.e(TAG, e.getMessage(), e);
}
viewHolder.star.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
final boolean isStarred = song.getStarred();
final String id = song.getId();
if (!isStarred) {
viewHolder.star.setImageDrawable(starDrawable);
song.setStarred(true);
} else {
viewHolder.star.setImageDrawable(starHollowDrawable);
song.setStarred(false);
}
}).start();
}
});
new Thread(new Runnable() {
@Override
public void run() {
MusicService musicService = MusicServiceFactory.getMusicService(SongView.this.context);
try {
if (!isStarred) {
musicService.star(id, null, null, SongView.this.context, null);
} else {
musicService.unstar(id, null, null, SongView.this.context, null);
}
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
}
}
}).start();
}
});
}
}
update();
@ -394,6 +410,13 @@ public class SongView extends UpdateView implements Checkable
}
}
int rating = song.getUserRating() == null ? 0 : song.getUserRating();
viewHolder.fiveStar1.setImageDrawable(rating > 0 ? starDrawable : starHollowDrawable);
viewHolder.fiveStar2.setImageDrawable(rating > 1 ? starDrawable : starHollowDrawable);
viewHolder.fiveStar3.setImageDrawable(rating > 2 ? starDrawable : starHollowDrawable);
viewHolder.fiveStar4.setImageDrawable(rating > 3 ? starDrawable : starHollowDrawable);
viewHolder.fiveStar5.setImageDrawable(rating > 4 ? starDrawable : starHollowDrawable);
boolean playing = downloadService.getCurrentPlaying() == downloadFile;
if (playing)

View File

@ -45,6 +45,8 @@ fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry = MusicDirectory.
if (this@toDomainEntity.publishDate != null) {
artist = dateFormat.format(this@toDomainEntity.publishDate!!.time)
}
userRating = this@toDomainEntity.userRating
averageRating = this@toDomainEntity.averageRating
}
fun List<MusicDirectoryChild>.toDomainEntityList() = this.map { it.toDomainEntity() }

View File

@ -10,5 +10,9 @@ enum class Feature(
/**
* Enables new image downloader implementation.
*/
NEW_IMAGE_DOWNLOADER(false)
NEW_IMAGE_DOWNLOADER(false),
/**
* Enables five star rating system.
*/
FIVE_STAR_RATING(false)
}

View File

@ -26,15 +26,93 @@
a:contentDescription="@string/albumArt"/>
<LinearLayout
a:id="@+id/download_visualizer_view_layout"
a:id="@+id/album_art_inside"
a:layout_width="fill_parent"
a:layout_height="60dip"
a:layout_gravity="bottom|center_horizontal"
a:layout_marginLeft="60dip"
a:layout_marginRight="60dip"
a:background="@color/translucent"
a:orientation="vertical"/>
a:layout_height="fill_parent"
a:gravity="bottom"
a:orientation="vertical" >
<LinearLayout
a:id="@+id/song_rating"
a:layout_width="match_parent"
a:layout_height="60dip"
a:layout_gravity="center"
a:layout_margin="40dip"
a:orientation="horizontal">
<ImageView
a:id="@+id/song_five_star_1"
a:layout_width="0dip"
a:layout_height="fill_parent"
a:layout_weight="1"
a:padding="10dip"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
a:scaleType="fitCenter"
a:src="?attr/star_hollow" />
<ImageView
a:id="@+id/song_five_star_2"
a:layout_width="0dip"
a:layout_height="fill_parent"
a:layout_weight="1"
a:padding="10dip"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
a:scaleType="fitCenter"
a:src="?attr/star_hollow" />
<ImageView
a:id="@+id/song_five_star_3"
a:layout_width="0dip"
a:layout_height="fill_parent"
a:layout_weight="1"
a:padding="10dip"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
a:scaleType="fitCenter"
a:src="?attr/star_hollow" />
<ImageView
a:id="@+id/song_five_star_4"
a:layout_width="0dip"
a:layout_height="fill_parent"
a:layout_weight="1"
a:padding="10dip"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
a:scaleType="fitCenter"
a:src="?attr/star_hollow" />
<ImageView
a:id="@+id/song_five_star_5"
a:layout_width="0dip"
a:layout_height="fill_parent"
a:layout_weight="1"
a:padding="10dip"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
a:scaleType="fitCenter"
a:src="?attr/star_hollow" />
</LinearLayout>
<LinearLayout
a:id="@+id/download_visualizer_view_layout"
a:layout_width="fill_parent"
a:layout_height="60dip"
a:layout_gravity="bottom|center_horizontal"
a:layout_marginLeft="60dip"
a:layout_marginRight="60dip"
a:background="@color/translucent"
a:orientation="vertical"/>
</LinearLayout>
</FrameLayout>
<include layout="@layout/download_playlist"/>

View File

@ -28,16 +28,94 @@
a:contentDescription="@string/albumArt"/>
<LinearLayout
a:id="@+id/download_visualizer_view_layout"
a:id="@+id/album_art_inside"
a:layout_width="fill_parent"
a:layout_height="60dip"
a:layout_alignParentBottom="true"
a:layout_gravity="center_horizontal"
a:background="@color/translucent"
a:layout_marginLeft="80dip"
a:layout_marginRight="80dip"
a:orientation="vertical"
/>
a:layout_height="fill_parent"
a:gravity="bottom"
a:orientation="vertical" >
<LinearLayout
a:id="@+id/song_rating"
a:layout_width="match_parent"
a:layout_height="60dip"
a:layout_gravity="center"
a:layout_margin="40dip"
a:orientation="horizontal">
<ImageView
a:id="@+id/song_five_star_1"
a:layout_width="0dip"
a:layout_height="fill_parent"
a:layout_weight="1"
a:padding="5dip"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
a:scaleType="fitCenter"
a:src="?attr/star_hollow" />
<ImageView
a:id="@+id/song_five_star_2"
a:layout_width="0dip"
a:layout_height="fill_parent"
a:layout_weight="1"
a:padding="5dip"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
a:scaleType="fitCenter"
a:src="?attr/star_hollow" />
<ImageView
a:id="@+id/song_five_star_3"
a:layout_width="0dip"
a:layout_height="fill_parent"
a:layout_weight="1"
a:padding="5dip"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
a:scaleType="fitCenter"
a:src="?attr/star_hollow" />
<ImageView
a:id="@+id/song_five_star_4"
a:layout_width="0dip"
a:layout_height="fill_parent"
a:layout_weight="1"
a:padding="5dip"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
a:scaleType="fitCenter"
a:src="?attr/star_hollow" />
<ImageView
a:id="@+id/song_five_star_5"
a:layout_width="0dip"
a:layout_height="fill_parent"
a:layout_weight="1"
a:padding="5dip"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
a:scaleType="fitCenter"
a:src="?attr/star_hollow" />
</LinearLayout>
<LinearLayout
a:id="@+id/download_visualizer_view_layout"
a:layout_width="fill_parent"
a:layout_height="60dip"
a:layout_gravity="center"
a:background="@color/translucent"
a:layout_marginLeft="80dip"
a:layout_marginRight="80dip"
a:orientation="vertical"
/>
</LinearLayout>
</RelativeLayout>
<include layout="@layout/download_playlist" />

View File

@ -4,14 +4,14 @@
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/statusbar"
android:layout_width="match_parent"
android:layout_height="128dp"
android:layout_height="150dp"
android:orientation="horizontal"
android:background="@color/background_color_dark" >
<ImageView
android:id="@+id/notification_image"
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_width="150dp"
android:layout_height="150dp"
android:gravity="center"
tools:background="#ff00ff"
/>
@ -41,7 +41,7 @@
android:layout_weight="1"
android:ellipsize="marquee"
android:focusable="true"
android:maxLines="1"
android:singleLine="true"
tools:text="Track name"
/>
@ -80,15 +80,75 @@
android:maxLines="1"
tools:text="Album"
/>
<LinearLayout
android:id="@+id/notification_rating"
android:layout_width="match_parent"
android:layout_height="30dip"
android:layout_gravity="center"
android:orientation="horizontal"
android:visibility="visible">
<ImageView
android:id="@+id/notification_five_star_1"
android:layout_width="0dip"
android:layout_height="fill_parent"
android:layout_weight="1"
android:background="@android:color/transparent"
android:focusable="false"
android:gravity="center_vertical"
android:scaleType="fitCenter" />
<ImageView
android:id="@+id/notification_five_star_2"
android:layout_width="0dip"
android:layout_height="fill_parent"
android:layout_weight="1"
android:background="@android:color/transparent"
android:focusable="false"
android:gravity="center_vertical"
android:scaleType="fitCenter" />
<ImageView
android:id="@+id/notification_five_star_3"
android:layout_width="0dip"
android:layout_height="fill_parent"
android:layout_weight="1"
android:background="@android:color/transparent"
android:focusable="false"
android:gravity="center_vertical"
android:scaleType="fitCenter" />
<ImageView
android:id="@+id/notification_five_star_4"
android:layout_width="0dip"
android:layout_height="fill_parent"
android:layout_weight="1"
android:background="@android:color/transparent"
android:focusable="false"
android:gravity="center_vertical"
android:scaleType="fitCenter" />
<ImageView
android:id="@+id/notification_five_star_5"
android:layout_width="0dip"
android:layout_height="fill_parent"
android:layout_weight="1"
android:background="@android:color/transparent"
android:focusable="false"
android:gravity="center_vertical"
android:scaleType="fitCenter" />
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="10dip"
android:layout_marginTop="5dip"
android:layout_marginBottom="10dip"
android:background="#DD696969"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -10,60 +10,59 @@
android:layout_height="4dip"
android:layout_width="fill_parent"
android:background="@drawable/drop_shadow" />
<LinearLayout
android:id="@+id/now_playing_view"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
android:layout_height="wrap_content">
<ImageView
android:id="@+id/now_playing_image"
android:layout_width="64.0dip"
android:layout_height="64.0dip"
android:focusable="true"
android:gravity="center" />
<ImageView
android:id="@+id/now_playing_image"
android:layout_width="64.0dip"
android:layout_height="64.0dip"
android:focusable="true"
android:gravity="center" />
<LinearLayout
android:layout_width="0.0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1.0"
android:orientation="vertical"
android:paddingLeft="11.0dip" >
<LinearLayout
android:layout_width="0.0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1.0"
android:orientation="vertical"
android:paddingLeft="11.0dip">
<TextView
android:id="@+id/now_playing_trackname"
<TextView
android:id="@+id/now_playing_trackname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:ellipsize="marquee"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold" />
<TextView
android:id="@+id/now_playing_artist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:ellipsize="end"
android:scrollHorizontally="true"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>
<ImageView
android:id="@+id/now_playing_control_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:ellipsize="marquee"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold" />
android:layout_gravity="center|right"
android:layout_marginTop="2dip"
android:layout_marginRight="5dip"
android:layout_weight="0.0"
android:focusable="false"
android:src="?attr/media_pause" />
<TextView
android:id="@+id/now_playing_artist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:ellipsize="end"
android:scrollHorizontally="true"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>
<ImageView
android:id="@+id/now_playing_control_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center|right"
android:layout_marginRight="5dip"
android:layout_marginTop="2dip"
android:layout_weight="0.0"
android:focusable="false"
android:src="?attr/media_pause" />
</LinearLayout>
</LinearLayout>

View File

@ -15,6 +15,66 @@
<include layout="@layout/song_details" />
<LinearLayout
a:id="@+id/song_rating"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_gravity="center_vertical"
a:orientation="horizontal">
<ImageView
a:id="@+id/song_five_star_1"
a:layout_width="10dip"
a:layout_height="fill_parent"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
a:scaleType="centerInside"
a:src="?attr/star_hollow" />
<ImageView
a:id="@+id/song_five_star_2"
a:layout_width="10dip"
a:layout_height="fill_parent"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
a:scaleType="centerInside"
a:src="?attr/star_hollow" />
<ImageView
a:id="@+id/song_five_star_3"
a:layout_width="10dip"
a:layout_height="fill_parent"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
a:scaleType="centerInside"
a:src="?attr/star_hollow" />
<ImageView
a:id="@+id/song_five_star_4"
a:layout_width="10dip"
a:layout_height="fill_parent"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
a:scaleType="centerInside"
a:src="?attr/star_hollow" />
<ImageView
a:id="@+id/song_five_star_5"
a:layout_width="10dip"
a:layout_height="fill_parent"
a:background="@android:color/transparent"
a:focusable="false"
a:gravity="center_vertical"
a:scaleType="centerInside"
a:layout_marginRight="3dip"
a:src="?attr/star_hollow" />
</LinearLayout>
<ImageView
a:id="@+id/song_star"
a:layout_width="wrap_content"

View File

@ -441,5 +441,9 @@
Actualmente no guarda la imagen en el almacenamiento del dispositivo y sólo utiliza caché en la memoria.
</string>
<string name="feature_flags_image_loader_title">Habilitar nuevo cargador de imágenes</string>
<string name="feature_flags_five_star_rating_title">Use cinco estrellas para las canciones.</string>
<string name="feature_flags_five_star_rating_description">Utilice el sistema de calificación de cinco estrellas para canciones
en lugar de simplemente destacar / desestimar elementos.
</string>
</resources>

View File

@ -442,6 +442,10 @@
Actuellement, il n\'enregistre pas l\'image dans le stockage de l\'appareil et n\'utilise que le cache en
mémoire.
</string>
<string name="feature_flags_five_star_rating_title">Utiliser une note de cinq étoiles pour les chansons</string>
<string name="feature_flags_five_star_rating_description">Utiliser un système de notation à cinq étoiles pour les chansons
au lieu de simplement mettre en vedette / désactiver les éléments.
</string>
<string name="feature_flags_category_title">Drapeaux des fonctionnalités</string>
</resources>

View File

@ -441,5 +441,9 @@
</string>
<string name="feature_flags_category_title">Jellemzők Zászlók</string>
<string name="feature_flags_image_loader_title">Engedélyezzen új képbetöltőt</string>
<string name="feature_flags_five_star_rating_title">Öt csillagos értékelés használata a dalokhoz</string>
<string name="feature_flags_five_star_rating_description">Öt csillag használata az értékeléshez az egyszerű
csillaggal jelölés helyett.
</string>
</resources>

View File

@ -442,5 +442,9 @@
Momenteel slaat het geen afbeeldingen op op de apparaatopslag en wordt alleen geheugencache gebruikt.
</string>
<string name="feature_flags_category_title">Experimentele functies</string>
<string name="feature_flags_five_star_rating_title">Gebruik vijf sterren voor nummers</string>
<string name="feature_flags_five_star_rating_description">Gebruik vijf sterren ratingsysteem voor liedjes
in plaats van items simpelweg in de hoofdrol te zetten / niet te verwijderen.
</string>
</resources>

View File

@ -455,5 +455,9 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników
<string name="feature_flags_image_loader_description">Włącza implementację modułu ładującego nowe obrazy.
Obecnie nie zapisuje obrazów w pamięci urządzenia, tylko wykorzystuje tylko pamięć podręczną.</string>
<string name="feature_flags_category_title">Flagi funkcji</string>
<string name="feature_flags_five_star_rating_title">Użyj pięciu gwiazdek dla utworów</string>
<string name="feature_flags_five_star_rating_description">W przypadku utworów użyj systemu pięciu gwiazdek
zamiast po prostu grać gwiazdkami / bez gwiazd.
</string>
</resources>

View File

@ -441,5 +441,9 @@
</string>
<string name="feature_flags_category_title">Bandeiras de recursos</string>
<string name="feature_flags_image_loader_title">Ativar novo carregador de imagens</string>
<string name="feature_flags_five_star_rating_title">Use classificação de cinco estrelas para músicas</string>
<string name="feature_flags_five_star_rating_description">Use o sistema de classificação de cinco estrelas para músicas
em vez de simplesmente estrelar / não estrelar itens.
</string>
</resources>

View File

@ -441,5 +441,9 @@
</string>
<string name="feature_flags_category_title">Bandeiras de recursos</string>
<string name="feature_flags_image_loader_title">Ativar novo carregador de imagens</string>
<string name="feature_flags_five_star_rating_title">Use classificação de cinco estrelas para músicas</string>
<string name="feature_flags_five_star_rating_description">Use o sistema de classificação de cinco estrelas para músicas
em vez de simplesmente estrelar / não estrelar itens.
</string>
</resources>

View File

@ -445,6 +445,10 @@
<string name="feature_flags_image_loader_description">Enables new image loader implementation.
Currently it doesn\'t save image in device storage and uses only cache in memory.
</string>
<string name="feature_flags_five_star_rating_title">Use five star rating for songs</string>
<string name="feature_flags_five_star_rating_description">Use five star rating system for songs
instead of simply starring/unstarring items.
</string>
<string name="feature_flags_category_title">Feature Flags</string>
</resources>

View File

@ -291,6 +291,12 @@
a:title="@string/feature_flags_image_loader_title"
a:summary="@string/feature_flags_image_loader_description"
/>
<CheckBoxPreference
a:key="use_five_star_rating"
a:persistent="false"
a:title="@string/feature_flags_five_star_rating_title"
a:summary="@string/feature_flags_five_star_rating_description"
/>
</PreferenceCategory>

View File

@ -39,7 +39,7 @@ class APIMusicDirectoryConverterTest {
transcodedSuffix = "some-transcoded-suffix", duration = 11, bitRate = 256,
path = "some-path", isDir = true, isVideo = true, playCount = 323, discNumber = 2,
created = Calendar.getInstance(), type = "some-type",
starred = Calendar.getInstance())
starred = Calendar.getInstance(), userRating = 3, averageRating = 2.99F)
val convertedEntity = entity.toDomainEntity()
@ -69,6 +69,8 @@ class APIMusicDirectoryConverterTest {
starred `should be equal to` (entity.starred != null)
discNumber `should equal` entity.discNumber
type `should equal` entity.type
userRating `should equal` entity.userRating
averageRating `should equal` entity.averageRating
}
}