Merge pull request #155 from tkashkin/lyrics
Add synchronized lyrics support (only LRC format at the moment)
This commit is contained in:
commit
77290b4ad3
15 changed files with 486 additions and 93 deletions
|
|
@ -2,22 +2,21 @@ package com.kabouzeid.gramophone.dialogs;
|
|||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
import com.kabouzeid.gramophone.model.lyrics.Lyrics;
|
||||
|
||||
/**
|
||||
* @author Karim Abou Zeid (kabouzeid)
|
||||
*/
|
||||
public class LyricsDialog extends DialogFragment {
|
||||
|
||||
public static LyricsDialog create(@NonNull LyricInfo lyricInfo) {
|
||||
public static LyricsDialog create(@NonNull Lyrics lyrics) {
|
||||
LyricsDialog dialog = new LyricsDialog();
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable("LyricInfo", lyricInfo);
|
||||
args.putString("title", lyrics.song.title);
|
||||
args.putString("lyrics", lyrics.getText());
|
||||
dialog.setArguments(args);
|
||||
return dialog;
|
||||
}
|
||||
|
|
@ -25,49 +24,10 @@ public class LyricsDialog extends DialogFragment {
|
|||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
LyricInfo lyricInfo = getArguments().getParcelable("LyricInfo");
|
||||
//noinspection ConstantConditions
|
||||
return new MaterialDialog.Builder(getActivity())
|
||||
.title(lyricInfo.title)
|
||||
.content(lyricInfo.lyrics)
|
||||
.title(getArguments().getString("title"))
|
||||
.content(getArguments().getString("lyrics"))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static class LyricInfo implements Parcelable {
|
||||
public final String title;
|
||||
public final String lyrics;
|
||||
|
||||
public LyricInfo(String title, String lyrics) {
|
||||
this.title = title;
|
||||
this.lyrics = lyrics;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(this.title);
|
||||
dest.writeString(this.lyrics);
|
||||
}
|
||||
|
||||
protected LyricInfo(Parcel in) {
|
||||
this.title = in.readString();
|
||||
this.lyrics = in.readString();
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<LyricInfo> CREATOR = new Parcelable.Creator<LyricInfo>() {
|
||||
@Override
|
||||
public LyricInfo createFromParcel(Parcel source) {
|
||||
return new LyricInfo(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LyricInfo[] newArray(int size) {
|
||||
return new LyricInfo[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ public class MusicProgressViewUpdateHelper extends Handler {
|
|||
private static final int UPDATE_INTERVAL_PAUSED = 500;
|
||||
|
||||
private Callback callback;
|
||||
private int intervalPlaying;
|
||||
private int intervalPaused;
|
||||
|
||||
public void start() {
|
||||
queueNextRefresh(1);
|
||||
|
|
@ -26,6 +28,14 @@ public class MusicProgressViewUpdateHelper extends Handler {
|
|||
|
||||
public MusicProgressViewUpdateHelper(Callback callback) {
|
||||
this.callback = callback;
|
||||
this.intervalPlaying = UPDATE_INTERVAL_PLAYING;
|
||||
this.intervalPaused = UPDATE_INTERVAL_PAUSED;
|
||||
}
|
||||
|
||||
public MusicProgressViewUpdateHelper(Callback callback, int intervalPlaying, int intervalPaused) {
|
||||
this.callback = callback;
|
||||
this.intervalPlaying = intervalPlaying;
|
||||
this.intervalPaused = intervalPaused;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -43,10 +53,10 @@ public class MusicProgressViewUpdateHelper extends Handler {
|
|||
callback.onUpdateProgressViews(progressMillis, totalMillis);
|
||||
|
||||
if (!MusicPlayerRemote.isPlaying()) {
|
||||
return UPDATE_INTERVAL_PAUSED;
|
||||
return intervalPaused;
|
||||
}
|
||||
|
||||
final int remainingMillis = UPDATE_INTERVAL_PLAYING - progressMillis % UPDATE_INTERVAL_PLAYING;
|
||||
final int remainingMillis = intervalPlaying - progressMillis % intervalPlaying;
|
||||
|
||||
return Math.max(MIN_INTERVAL, remainingMillis);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
package com.kabouzeid.gramophone.model.lyrics;
|
||||
|
||||
import android.util.SparseArray;
|
||||
|
||||
public abstract class AbsSynchronizedLyrics extends Lyrics {
|
||||
private static final int TIME_OFFSET_MS = 500; // time adjustment to display line before it actually starts
|
||||
|
||||
protected final SparseArray<String> lines = new SparseArray<>();
|
||||
protected int offset = 0;
|
||||
|
||||
public String getLine(int time) {
|
||||
time += offset + AbsSynchronizedLyrics.TIME_OFFSET_MS;
|
||||
|
||||
int lastLineTime = lines.keyAt(0);
|
||||
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
int lineTime = lines.keyAt(i);
|
||||
|
||||
if (time >= lineTime) {
|
||||
lastLineTime = lineTime;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return lines.get(lastLineTime);
|
||||
}
|
||||
|
||||
public boolean isSynchronized() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
parse(true);
|
||||
return valid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
parse(false);
|
||||
|
||||
if (valid) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
String line = lines.valueAt(i);
|
||||
sb.append(line).append("\r\n");
|
||||
}
|
||||
|
||||
return sb.toString().trim().replaceAll("(\r?\n){3,}", "\r\n\r\n");
|
||||
}
|
||||
|
||||
return super.getText();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.kabouzeid.gramophone.model.lyrics;
|
||||
|
||||
import com.kabouzeid.gramophone.model.Song;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class Lyrics {
|
||||
private static final ArrayList<Class<? extends Lyrics>> FORMATS = new ArrayList<>();
|
||||
|
||||
public Song song;
|
||||
public String data;
|
||||
|
||||
protected boolean parsed = false;
|
||||
protected boolean valid = false;
|
||||
|
||||
public Lyrics setData(Song song, String data) {
|
||||
this.song = song;
|
||||
this.data = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
public static Lyrics parse(Song song, String data) {
|
||||
for (Class<? extends Lyrics> format : Lyrics.FORMATS) {
|
||||
try {
|
||||
Lyrics lyrics = format.newInstance().setData(song, data);
|
||||
if (lyrics.isValid()) return lyrics.parse(false);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return new Lyrics().setData(song, data).parse(false);
|
||||
}
|
||||
|
||||
public static boolean isSynchronized(String data) {
|
||||
for (Class<? extends Lyrics> format : Lyrics.FORMATS) {
|
||||
try {
|
||||
Lyrics lyrics = format.newInstance().setData(null, data);
|
||||
if (lyrics.isValid()) return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Lyrics parse(boolean check) {
|
||||
this.valid = true;
|
||||
this.parsed = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isSynchronized() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
this.parse(true);
|
||||
return this.valid;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return this.data.trim().replaceAll("(\r?\n){3,}", "\r\n\r\n");
|
||||
}
|
||||
|
||||
static {
|
||||
Lyrics.FORMATS.add(SynchronizedLyricsLRC.class);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package com.kabouzeid.gramophone.model.lyrics;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
class SynchronizedLyricsLRC extends AbsSynchronizedLyrics {
|
||||
private static final Pattern LRC_LINE_PATTERN = Pattern.compile("((?:\\[.*?\\])+)(.*)");
|
||||
private static final Pattern LRC_TIME_PATTERN = Pattern.compile("\\[(\\d+):(\\d{2}(?:\\.\\d+)?)\\]");
|
||||
private static final Pattern LRC_ATTRIBUTE_PATTERN = Pattern.compile("\\[(\\D+):(.+)\\]");
|
||||
|
||||
private static final float LRC_SECONDS_TO_MS_MULTIPLIER = 1000f;
|
||||
private static final int LRC_MINUTES_TO_MS_MULTIPLIER = 60000;
|
||||
|
||||
@Override
|
||||
public SynchronizedLyricsLRC parse(boolean check) {
|
||||
if (this.parsed || this.data == null || this.data.isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
|
||||
String[] lines = this.data.split("\r?\n");
|
||||
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Matcher attrMatcher = SynchronizedLyricsLRC.LRC_ATTRIBUTE_PATTERN.matcher(line);
|
||||
if (attrMatcher.find()) {
|
||||
try {
|
||||
String attr = attrMatcher.group(1).toLowerCase().trim();
|
||||
String value = attrMatcher.group(2).toLowerCase().trim();
|
||||
switch (attr) {
|
||||
case "offset":
|
||||
this.offset = Integer.parseInt(value);
|
||||
break;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
Matcher matcher = SynchronizedLyricsLRC.LRC_LINE_PATTERN.matcher(line);
|
||||
if (matcher.find()) {
|
||||
String time = matcher.group(1);
|
||||
String text = matcher.group(2);
|
||||
|
||||
Matcher timeMatcher = SynchronizedLyricsLRC.LRC_TIME_PATTERN.matcher(time);
|
||||
while (timeMatcher.find()) {
|
||||
int m = 0;
|
||||
float s = 0f;
|
||||
try {
|
||||
m = Integer.parseInt(timeMatcher.group(1));
|
||||
s = Float.parseFloat(timeMatcher.group(2));
|
||||
} catch (NumberFormatException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
int ms = (int) (s * LRC_SECONDS_TO_MS_MULTIPLIER) + m * LRC_MINUTES_TO_MS_MULTIPLIER;
|
||||
|
||||
this.valid = true;
|
||||
if (check) return this;
|
||||
|
||||
this.lines.append(ms, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.parsed = true;
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,13 +10,19 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.kabouzeid.gramophone.R;
|
||||
import com.kabouzeid.gramophone.adapter.AlbumCoverPagerAdapter;
|
||||
import com.kabouzeid.gramophone.helper.MusicPlayerRemote;
|
||||
import com.kabouzeid.gramophone.helper.MusicProgressViewUpdateHelper;
|
||||
import com.kabouzeid.gramophone.misc.SimpleAnimatorListener;
|
||||
import com.kabouzeid.gramophone.model.lyrics.AbsSynchronizedLyrics;
|
||||
import com.kabouzeid.gramophone.model.lyrics.Lyrics;
|
||||
import com.kabouzeid.gramophone.ui.fragments.AbsMusicServiceFragment;
|
||||
import com.kabouzeid.gramophone.util.PreferenceUtil;
|
||||
import com.kabouzeid.gramophone.util.ViewUtil;
|
||||
|
||||
import butterknife.BindView;
|
||||
|
|
@ -26,9 +32,11 @@ import butterknife.Unbinder;
|
|||
/**
|
||||
* @author Karim Abou Zeid (kabouzeid)
|
||||
*/
|
||||
public class PlayerAlbumCoverFragment extends AbsMusicServiceFragment implements ViewPager.OnPageChangeListener {
|
||||
public class PlayerAlbumCoverFragment extends AbsMusicServiceFragment implements ViewPager.OnPageChangeListener, MusicProgressViewUpdateHelper.Callback {
|
||||
public static final String TAG = PlayerAlbumCoverFragment.class.getSimpleName();
|
||||
|
||||
public static final int LYRICS_ANIM_DURATION = 300;
|
||||
|
||||
private Unbinder unbinder;
|
||||
|
||||
@BindView(R.id.player_album_cover_viewpager)
|
||||
|
|
@ -36,9 +44,19 @@ public class PlayerAlbumCoverFragment extends AbsMusicServiceFragment implements
|
|||
@BindView(R.id.player_favorite_icon)
|
||||
ImageView favoriteIcon;
|
||||
|
||||
@BindView(R.id.player_lyrics)
|
||||
FrameLayout lyricsLayout;
|
||||
@BindView(R.id.player_lyrics_line1)
|
||||
TextView lyricsLine1;
|
||||
@BindView(R.id.player_lyrics_line2)
|
||||
TextView lyricsLine2;
|
||||
|
||||
private Callbacks callbacks;
|
||||
private int currentPosition;
|
||||
|
||||
private Lyrics lyrics;
|
||||
private MusicProgressViewUpdateHelper progressViewUpdateHelper;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
|
@ -68,12 +86,15 @@ public class PlayerAlbumCoverFragment extends AbsMusicServiceFragment implements
|
|||
return gestureDetector.onTouchEvent(event);
|
||||
}
|
||||
});
|
||||
progressViewUpdateHelper = new MusicProgressViewUpdateHelper(this, 500, 1000);
|
||||
progressViewUpdateHelper.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
viewPager.removeOnPageChangeListener(this);
|
||||
progressViewUpdateHelper.stop();
|
||||
unbinder.unbind();
|
||||
}
|
||||
|
||||
|
|
@ -163,6 +184,43 @@ public class PlayerAlbumCoverFragment extends AbsMusicServiceFragment implements
|
|||
.start();
|
||||
}
|
||||
|
||||
private boolean isLyricsLayoutVisible() {
|
||||
return lyrics != null && lyrics.isSynchronized() && lyrics.isValid() && PreferenceUtil.getInstance(getActivity()).synchronizedLyricsShow();
|
||||
}
|
||||
|
||||
private boolean isLyricsLayoutBound() {
|
||||
return lyricsLayout != null && lyricsLine1 != null && lyricsLine2 != null;
|
||||
}
|
||||
|
||||
private void hideLyricsLayout() {
|
||||
lyricsLayout.animate().alpha(0f).setDuration(PlayerAlbumCoverFragment.LYRICS_ANIM_DURATION).withEndAction(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isLyricsLayoutBound()) return;
|
||||
lyricsLayout.setVisibility(View.GONE);
|
||||
lyricsLine1.setText(null);
|
||||
lyricsLine2.setText(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setLyrics(Lyrics l) {
|
||||
lyrics = l;
|
||||
|
||||
if (!isLyricsLayoutBound()) return;
|
||||
|
||||
if (!isLyricsLayoutVisible()) {
|
||||
hideLyricsLayout();
|
||||
return;
|
||||
}
|
||||
|
||||
lyricsLine1.setText(null);
|
||||
lyricsLine2.setText(null);
|
||||
|
||||
lyricsLayout.setVisibility(View.VISIBLE);
|
||||
lyricsLayout.animate().alpha(1f).setDuration(PlayerAlbumCoverFragment.LYRICS_ANIM_DURATION);
|
||||
}
|
||||
|
||||
private void notifyColorChange(int color) {
|
||||
if (callbacks != null) callbacks.onColorChanged(color);
|
||||
}
|
||||
|
|
@ -171,6 +229,44 @@ public class PlayerAlbumCoverFragment extends AbsMusicServiceFragment implements
|
|||
callbacks = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdateProgressViews(int progress, int total) {
|
||||
if (!isLyricsLayoutBound()) return;
|
||||
|
||||
if (!isLyricsLayoutVisible()) {
|
||||
hideLyricsLayout();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(lyrics instanceof AbsSynchronizedLyrics)) return;
|
||||
AbsSynchronizedLyrics synchronizedLyrics = (AbsSynchronizedLyrics) lyrics;
|
||||
|
||||
lyricsLayout.setVisibility(View.VISIBLE);
|
||||
lyricsLayout.setAlpha(1f);
|
||||
|
||||
String oldLine = lyricsLine2.getText().toString();
|
||||
String line = synchronizedLyrics.getLine(progress);
|
||||
|
||||
if (!oldLine.equals(line) || oldLine.isEmpty()) {
|
||||
lyricsLine1.setText(oldLine);
|
||||
lyricsLine2.setText(line);
|
||||
|
||||
lyricsLine1.setVisibility(View.VISIBLE);
|
||||
lyricsLine2.setVisibility(View.VISIBLE);
|
||||
|
||||
lyricsLine2.measure(View.MeasureSpec.makeMeasureSpec(lyricsLine2.getMeasuredWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.UNSPECIFIED);
|
||||
int h = lyricsLine2.getMeasuredHeight();
|
||||
|
||||
lyricsLine1.setAlpha(1f);
|
||||
lyricsLine1.setTranslationY(0f);
|
||||
lyricsLine1.animate().alpha(0f).translationY(-h).setDuration(PlayerAlbumCoverFragment.LYRICS_ANIM_DURATION);
|
||||
|
||||
lyricsLine2.setAlpha(0f);
|
||||
lyricsLine2.setTranslationY(h);
|
||||
lyricsLine2.animate().alpha(1f).translationY(0f).setDuration(PlayerAlbumCoverFragment.LYRICS_ANIM_DURATION);
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callbacks {
|
||||
void onColorChanged(int color);
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import com.kabouzeid.gramophone.dialogs.SongShareDialog;
|
|||
import com.kabouzeid.gramophone.helper.MusicPlayerRemote;
|
||||
import com.kabouzeid.gramophone.helper.menu.SongMenuHelper;
|
||||
import com.kabouzeid.gramophone.model.Song;
|
||||
import com.kabouzeid.gramophone.model.lyrics.Lyrics;
|
||||
import com.kabouzeid.gramophone.ui.activities.base.AbsSlidingMusicPanelActivity;
|
||||
import com.kabouzeid.gramophone.ui.fragments.player.AbsPlayerFragment;
|
||||
import com.kabouzeid.gramophone.ui.fragments.player.PlayerAlbumCoverFragment;
|
||||
|
|
@ -51,11 +52,6 @@ import com.kabouzeid.gramophone.util.ViewUtil;
|
|||
import com.kabouzeid.gramophone.views.WidthFitSquareLayout;
|
||||
import com.sothree.slidinguppanel.SlidingUpPanelLayout;
|
||||
|
||||
import org.jaudiotagger.audio.AudioFileIO;
|
||||
import org.jaudiotagger.tag.FieldKey;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.Unbinder;
|
||||
|
|
@ -93,7 +89,7 @@ public class CardPlayerFragment extends AbsPlayerFragment implements PlayerAlbum
|
|||
private AsyncTask updateIsFavoriteTask;
|
||||
private AsyncTask updateLyricsAsyncTask;
|
||||
|
||||
private LyricsDialog.LyricInfo lyricsInfo;
|
||||
private Lyrics lyrics;
|
||||
|
||||
private Impl impl;
|
||||
|
||||
|
|
@ -240,8 +236,8 @@ public class CardPlayerFragment extends AbsPlayerFragment implements PlayerAlbum
|
|||
public boolean onMenuItemClick(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_show_lyrics:
|
||||
if (lyricsInfo != null)
|
||||
LyricsDialog.create(lyricsInfo).show(getFragmentManager(), "LYRICS");
|
||||
if (lyrics != null)
|
||||
LyricsDialog.create(lyrics).show(getFragmentManager(), "LYRICS");
|
||||
return true;
|
||||
}
|
||||
return super.onMenuItemClick(item);
|
||||
|
|
@ -304,35 +300,33 @@ public class CardPlayerFragment extends AbsPlayerFragment implements PlayerAlbum
|
|||
private void updateLyrics() {
|
||||
if (updateLyricsAsyncTask != null) updateLyricsAsyncTask.cancel(false);
|
||||
final Song song = MusicPlayerRemote.getCurrentSong();
|
||||
updateLyricsAsyncTask = new AsyncTask<Void, Void, String>() {
|
||||
updateLyricsAsyncTask = new AsyncTask<Void, Void, Lyrics>() {
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
lyricsInfo = null;
|
||||
lyrics = null;
|
||||
playerAlbumCoverFragment.setLyrics(null);
|
||||
toolbar.getMenu().removeItem(R.id.action_show_lyrics);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(Void... params) {
|
||||
try {
|
||||
return AudioFileIO.read(new File(song.data)).getTagOrCreateDefault().getFirst(FieldKey.LYRICS);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
cancel(false);
|
||||
protected Lyrics doInBackground(Void... params) {
|
||||
String data = MusicUtil.getLyrics(song);
|
||||
if (TextUtils.isEmpty(data)) {
|
||||
return null;
|
||||
}
|
||||
return Lyrics.parse(song, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String lyrics) {
|
||||
super.onPostExecute(lyrics);
|
||||
if (TextUtils.isEmpty(lyrics)) {
|
||||
lyricsInfo = null;
|
||||
protected void onPostExecute(Lyrics l) {
|
||||
lyrics = l;
|
||||
playerAlbumCoverFragment.setLyrics(lyrics);
|
||||
if (lyrics == null) {
|
||||
if (toolbar != null) {
|
||||
toolbar.getMenu().removeItem(R.id.action_show_lyrics);
|
||||
}
|
||||
} else {
|
||||
lyricsInfo = new LyricsDialog.LyricInfo(song.title, lyrics);
|
||||
Activity activity = getActivity();
|
||||
if (toolbar != null && activity != null)
|
||||
if (toolbar.getMenu().findItem(R.id.action_show_lyrics) == null) {
|
||||
|
|
@ -347,7 +341,7 @@ public class CardPlayerFragment extends AbsPlayerFragment implements PlayerAlbum
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled(String s) {
|
||||
protected void onCancelled(Lyrics s) {
|
||||
onPostExecute(null);
|
||||
}
|
||||
}.execute();
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import com.kabouzeid.gramophone.dialogs.SongShareDialog;
|
|||
import com.kabouzeid.gramophone.helper.MusicPlayerRemote;
|
||||
import com.kabouzeid.gramophone.helper.menu.SongMenuHelper;
|
||||
import com.kabouzeid.gramophone.model.Song;
|
||||
import com.kabouzeid.gramophone.model.lyrics.Lyrics;
|
||||
import com.kabouzeid.gramophone.ui.activities.base.AbsSlidingMusicPanelActivity;
|
||||
import com.kabouzeid.gramophone.ui.fragments.player.AbsPlayerFragment;
|
||||
import com.kabouzeid.gramophone.ui.fragments.player.PlayerAlbumCoverFragment;
|
||||
|
|
@ -49,11 +50,6 @@ import com.kabouzeid.gramophone.util.ViewUtil;
|
|||
import com.kabouzeid.gramophone.views.WidthFitSquareLayout;
|
||||
import com.sothree.slidinguppanel.SlidingUpPanelLayout;
|
||||
|
||||
import org.jaudiotagger.audio.AudioFileIO;
|
||||
import org.jaudiotagger.tag.FieldKey;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.Unbinder;
|
||||
|
|
@ -90,7 +86,7 @@ public class FlatPlayerFragment extends AbsPlayerFragment implements PlayerAlbum
|
|||
private AsyncTask updateIsFavoriteTask;
|
||||
private AsyncTask updateLyricsAsyncTask;
|
||||
|
||||
private LyricsDialog.LyricInfo lyricsInfo;
|
||||
private Lyrics lyrics;
|
||||
|
||||
private Impl impl;
|
||||
|
||||
|
|
@ -236,8 +232,8 @@ public class FlatPlayerFragment extends AbsPlayerFragment implements PlayerAlbum
|
|||
public boolean onMenuItemClick(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_show_lyrics:
|
||||
if (lyricsInfo != null)
|
||||
LyricsDialog.create(lyricsInfo).show(getFragmentManager(), "LYRICS");
|
||||
if (lyrics != null)
|
||||
LyricsDialog.create(lyrics).show(getFragmentManager(), "LYRICS");
|
||||
return true;
|
||||
}
|
||||
return super.onMenuItemClick(item);
|
||||
|
|
@ -300,35 +296,33 @@ public class FlatPlayerFragment extends AbsPlayerFragment implements PlayerAlbum
|
|||
private void updateLyrics() {
|
||||
if (updateLyricsAsyncTask != null) updateLyricsAsyncTask.cancel(false);
|
||||
final Song song = MusicPlayerRemote.getCurrentSong();
|
||||
updateLyricsAsyncTask = new AsyncTask<Void, Void, String>() {
|
||||
updateLyricsAsyncTask = new AsyncTask<Void, Void, Lyrics>() {
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
lyricsInfo = null;
|
||||
lyrics = null;
|
||||
playerAlbumCoverFragment.setLyrics(null);
|
||||
toolbar.getMenu().removeItem(R.id.action_show_lyrics);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(Void... params) {
|
||||
try {
|
||||
return AudioFileIO.read(new File(song.data)).getTagOrCreateDefault().getFirst(FieldKey.LYRICS);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
cancel(false);
|
||||
protected Lyrics doInBackground(Void... params) {
|
||||
String data = MusicUtil.getLyrics(song);
|
||||
if (TextUtils.isEmpty(data)) {
|
||||
return null;
|
||||
}
|
||||
return Lyrics.parse(song, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String lyrics) {
|
||||
super.onPostExecute(lyrics);
|
||||
if (TextUtils.isEmpty(lyrics)) {
|
||||
lyricsInfo = null;
|
||||
protected void onPostExecute(Lyrics l) {
|
||||
lyrics = l;
|
||||
playerAlbumCoverFragment.setLyrics(lyrics);
|
||||
if (lyrics == null) {
|
||||
if (toolbar != null) {
|
||||
toolbar.getMenu().removeItem(R.id.action_show_lyrics);
|
||||
}
|
||||
} else {
|
||||
lyricsInfo = new LyricsDialog.LyricInfo(song.title, lyrics);
|
||||
Activity activity = getActivity();
|
||||
if (toolbar != null && activity != null)
|
||||
if (toolbar.getMenu().findItem(R.id.action_show_lyrics) == null) {
|
||||
|
|
@ -343,7 +337,7 @@ public class FlatPlayerFragment extends AbsPlayerFragment implements PlayerAlbum
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled(String s) {
|
||||
protected void onCancelled(Lyrics s) {
|
||||
onPostExecute(null);
|
||||
}
|
||||
}.execute();
|
||||
|
|
|
|||
|
|
@ -11,9 +11,13 @@ import com.kabouzeid.gramophone.loader.SongLoader;
|
|||
import com.kabouzeid.gramophone.loader.SortedCursor;
|
||||
import com.kabouzeid.gramophone.model.Song;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
|
@ -160,4 +164,30 @@ public final class FileUtil {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String stripExtension(String str) {
|
||||
if (str == null) return null;
|
||||
int pos = str.lastIndexOf('.');
|
||||
if (pos == -1) return str;
|
||||
return str.substring(0, pos);
|
||||
}
|
||||
|
||||
public static String readFromStream(InputStream is) throws Exception {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (sb.length() > 0) sb.append("\n");
|
||||
sb.append(line);
|
||||
}
|
||||
reader.close();
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String read(File file) throws Exception {
|
||||
FileInputStream fin = new FileInputStream(file);
|
||||
String ret = readFromStream(fin);
|
||||
fin.close();
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,10 +24,17 @@ import com.kabouzeid.gramophone.loader.SongLoader;
|
|||
import com.kabouzeid.gramophone.model.Artist;
|
||||
import com.kabouzeid.gramophone.model.Playlist;
|
||||
import com.kabouzeid.gramophone.model.Song;
|
||||
import com.kabouzeid.gramophone.model.lyrics.AbsSynchronizedLyrics;
|
||||
|
||||
import org.jaudiotagger.audio.AudioFileIO;
|
||||
import org.jaudiotagger.tag.FieldKey;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author Karim Abou Zeid (kabouzeid)
|
||||
|
|
@ -255,4 +262,59 @@ public class MusicUtil {
|
|||
if (musicMediaTitle.isEmpty()) return "";
|
||||
return String.valueOf(musicMediaTitle.charAt(0)).toUpperCase();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getLyrics(Song song) {
|
||||
String lyrics = null;
|
||||
|
||||
File file = new File(song.data);
|
||||
|
||||
try {
|
||||
lyrics = AudioFileIO.read(file).getTagOrCreateDefault().getFirst(FieldKey.LYRICS);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (lyrics == null || lyrics.trim().isEmpty() || !AbsSynchronizedLyrics.isSynchronized(lyrics)) {
|
||||
File dir = file.getAbsoluteFile().getParentFile();
|
||||
|
||||
if (dir != null && dir.exists() && dir.isDirectory()) {
|
||||
String format = ".*%s.*\\.(lrc|txt)";
|
||||
String filename = Pattern.quote(FileUtil.stripExtension(file.getName()));
|
||||
String songtitle = Pattern.quote(song.title);
|
||||
|
||||
final ArrayList<Pattern> patterns = new ArrayList<>();
|
||||
patterns.add(Pattern.compile(String.format(format, filename), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE));
|
||||
patterns.add(Pattern.compile(String.format(format, songtitle), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE));
|
||||
|
||||
File[] files = dir.listFiles(new FileFilter() {
|
||||
@Override
|
||||
public boolean accept(File f) {
|
||||
for (Pattern pattern : patterns) {
|
||||
if (pattern.matcher(f.getName()).matches()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (files != null && files.length > 0) {
|
||||
for (File f : files) {
|
||||
try {
|
||||
String newLyrics = FileUtil.read(f);
|
||||
if (newLyrics != null && !newLyrics.trim().isEmpty()) {
|
||||
if (AbsSynchronizedLyrics.isSynchronized(newLyrics)) {
|
||||
return newLyrics;
|
||||
}
|
||||
lyrics = newLyrics;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lyrics;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ public final class PreferenceUtil {
|
|||
|
||||
public static final String START_DIRECTORY = "start_directory";
|
||||
|
||||
public static final String SYNCHRONIZED_LYRICS_SHOW = "synchronized_lyrics_show";
|
||||
|
||||
private static PreferenceUtil sInstance;
|
||||
|
||||
private final SharedPreferences mPreferences;
|
||||
|
|
@ -400,4 +402,8 @@ public final class PreferenceUtil {
|
|||
editor.putString(START_DIRECTORY, file.getPath());
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public final boolean synchronizedLyricsShow() {
|
||||
return mPreferences.getBoolean(SYNCHRONIZED_LYRICS_SHOW, true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,42 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/player_lyrics"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@drawable/shadow_up"
|
||||
android:padding="16dp"
|
||||
android:clipToPadding="false"
|
||||
android:visibility="gone"
|
||||
android:alpha="0">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/player_lyrics_line1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:textAlignment="center"
|
||||
android:textSize="22sp"
|
||||
android:textColor="@color/md_white_1000"
|
||||
android:shadowColor="@color/md_black_1000"
|
||||
android:shadowRadius="4"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/player_lyrics_line2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:textAlignment="center"
|
||||
android:textSize="22sp"
|
||||
android:textColor="@color/md_white_1000"
|
||||
android:shadowColor="@color/md_black_1000"
|
||||
android:shadowRadius="4" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<android.support.v7.widget.AppCompatImageView
|
||||
android:id="@+id/player_favorite_icon"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@
|
|||
<string name="pref_title_playback_controller_card_now_playing">Показывать подложку под кнопками</string>
|
||||
<string name="pref_title_colored_playback_controls_now_playing">Окрашенные кнопки управления воспроизведением</string>
|
||||
<string name="pref_title_audio_ducking">Уменьшить громкость при уведомлених</string>
|
||||
<string name="pref_title_synchronized_lyrics_show">Показывать синхронизированные тексты</string>
|
||||
<string name="no_equalizer">Эквалайзер не найден.</string>
|
||||
<string name="no_audio_ID">"Сначала воспроизведите песню, а затем попробуйте снова."</string>
|
||||
<string name="delete_action">Удалить</string>
|
||||
|
|
@ -173,6 +174,7 @@
|
|||
<string name="pref_summary_colored_navigation_bar">Окрашивает панель навигации в основной цвет.</string>
|
||||
<string name="pref_summary_colored_app_shortcuts">Окрашивает шорткаты в основной цвет.</string>
|
||||
<string name="pref_summary_audio_ducking">Уведомления, навигация, т.д.</string>
|
||||
<string name="pref_summary_synchronized_lyrics_show">Показывать синхронизированные тексты песен при их наличии</string>
|
||||
<string name="could_not_download_album_cover">"\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u043f\u043e\u0434\u0445\u043e\u0434\u044f\u0449\u0443\u044e \u043e\u0431\u043b\u043e\u0436\u043a\u0443 \u0430\u043b\u044c\u0431\u043e\u043c\u0430."</string>
|
||||
<string name="search_hint">Поиск библиотеки...</string>
|
||||
<string name="rescanning_media">Повторное сканирование медиа...</string>
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@
|
|||
<string name="pref_title_colored_playback_controls_now_playing">Colored playback controls</string>
|
||||
<string name="pref_title_audio_ducking">Reduce volume on focus loss</string>
|
||||
<string name="pref_title_last_added_interval">Last added playlist interval</string>
|
||||
<string name="pref_title_synchronized_lyrics_show">Show synchronized lyrics</string>
|
||||
<string name="no_equalizer">No equalizer found.</string>
|
||||
<string name="no_audio_ID">"Play a song first, then try again."</string>
|
||||
<string name="delete_action">Delete</string>
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
<string name="pref_summary_colored_navigation_bar">Colors the navigation bar in the primary color.</string>
|
||||
<string name="pref_summary_colored_app_shortcuts">Colors the app shortcuts in the primary color.</string>
|
||||
<string name="pref_summary_audio_ducking">Notifications, navigation etc.</string>
|
||||
<string name="pref_summary_synchronized_lyrics_show">Show synchronized lyrics for a song if available</string>
|
||||
<string name="could_not_download_album_cover">"Couldn\u2019t download a matching album cover."</string>
|
||||
<string name="search_hint">Search your library…</string>
|
||||
<string name="rescanning_media">Rescanning media…</string>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@
|
|||
android:key="now_playing_screen_id"
|
||||
android:title="@string/pref_title_now_playing_screen_appearance" />
|
||||
|
||||
<com.kabouzeid.appthemehelper.common.prefs.supportv7.ATESwitchPreference
|
||||
android:defaultValue="true"
|
||||
android:key="synchronized_lyrics_show"
|
||||
android:summary="@string/pref_summary_synchronized_lyrics_show"
|
||||
android:title="@string/pref_title_synchronized_lyrics_show" />
|
||||
|
||||
</com.kabouzeid.appthemehelper.common.prefs.supportv7.ATEPreferenceCategory>
|
||||
|
||||
</android.support.v7.preference.PreferenceScreen>
|
||||
Loading…
Add table
Add a link
Reference in a new issue