feat: implemented synchronized lyrics display

This commit is contained in:
CappielloAntonio 2024-02-17 23:44:49 +01:00
parent 111a17350b
commit e35aed9cc4
7 changed files with 240 additions and 24 deletions

View file

@ -249,6 +249,11 @@ public class PlayerBottomSheetFragment extends Fragment {
bind.playerBodyLayout.playerBodyBottomSheetViewPager.setCurrentItem(1, true);
}
public void setPlayerControllerVerticalPagerDraggableState(Boolean isDraggable) {
ViewPager2 playerControllerVerticalPager = (ViewPager2) bind.playerBodyLayout.playerBodyBottomSheetViewPager;
playerControllerVerticalPager.setUserInputEnabled(isDraggable);
}
private void defineProgressBarHandler(MediaBrowser mediaBrowser) {
progressBarHandler = new Handler();
progressBarRunnable = () -> {

View file

@ -257,10 +257,20 @@ public class PlayerControllerFragment extends Fragment {
public void onPageSelected(int position) {
super.onPageSelected(position);
PlayerBottomSheetFragment playerBottomSheetFragment = (PlayerBottomSheetFragment) requireActivity().getSupportFragmentManager().findFragmentByTag("PlayerBottomSheet");
if (position == 0) {
activity.setBottomSheetDraggableState(true);
if (playerBottomSheetFragment != null) {
playerBottomSheetFragment.setPlayerControllerVerticalPagerDraggableState(true);
}
} else if (position == 1) {
activity.setBottomSheetDraggableState(false);
if (playerBottomSheetFragment != null) {
playerBottomSheetFragment.setPlayerControllerVerticalPagerDraggableState(false);
}
}
}
});

View file

@ -1,24 +1,50 @@
package com.cappielloantonio.tempo.ui.fragment;
import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.os.Bundle;
import android.os.Handler;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.session.MediaBrowser;
import androidx.media3.session.SessionToken;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerLyricsBinding;
import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.subsonic.models.Line;
import com.cappielloantonio.tempo.subsonic.models.LyricsList;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil;
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.List;
@OptIn(markerClass = UnstableApi.class)
public class PlayerLyricsFragment extends Fragment {
private static final String TAG = "PlayerLyricsFragment";
private InnerFragmentPlayerLyricsBinding bind;
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
private MediaBrowser mediaBrowser;
private Handler syncLyricsHandler;
private Runnable syncLyricsRunnable;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@ -33,7 +59,32 @@ public class PlayerLyricsFragment extends Fragment {
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initLyrics();
initPanelContent();
}
@Override
public void onStart() {
super.onStart();
initializeBrowser();
}
@Override
public void onResume() {
super.onResume();
bindMediaController();
}
@Override
public void onPause() {
super.onPause();
releaseHandler();
}
@Override
public void onStop() {
releaseBrowser();
super.onStop();
}
@Override
@ -42,27 +93,156 @@ public class PlayerLyricsFragment extends Fragment {
bind = null;
}
private void initLyrics() {
playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> {
playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> {
if (bind != null) {
if (lyrics != null && !lyrics.trim().equals("")) {
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(lyrics));
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
bind.emptyDescriptionImageView.setVisibility(View.GONE);
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
} else if (description != null && !description.trim().equals("")) {
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(description));
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
bind.emptyDescriptionImageView.setVisibility(View.GONE);
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
} else {
bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE);
bind.emptyDescriptionImageView.setVisibility(View.VISIBLE);
bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE);
}
}
private void initializeBrowser() {
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
}
private void releaseHandler() {
if (syncLyricsHandler != null) {
syncLyricsHandler.removeCallbacks(syncLyricsRunnable);
syncLyricsHandler = null;
}
}
private void releaseBrowser() {
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
}
private void bindMediaController() {
mediaBrowserListenableFuture.addListener(() -> {
try {
mediaBrowser = mediaBrowserListenableFuture.get();
defineProgressHandler();
} catch (Exception e) {
e.printStackTrace();
}
}, MoreExecutors.directExecutor());
}
private void initPanelContent() {
if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) {
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
setPanelContent(null, lyricsList);
});
} else {
playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> {
setPanelContent(lyrics, null);
});
}
}
private void setPanelContent(String lyrics, LyricsList lyricsList) {
playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> {
if (bind != null) {
if (lyrics != null && !lyrics.trim().equals("")) {
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(lyrics));
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
bind.emptyDescriptionImageView.setVisibility(View.GONE);
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
} else if (lyricsList != null && lyricsList.getStructuredLyrics() != null) {
setSyncLirics(lyricsList);
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
bind.emptyDescriptionImageView.setVisibility(View.GONE);
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
} else if (description != null && !description.trim().equals("")) {
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(description));
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
bind.emptyDescriptionImageView.setVisibility(View.GONE);
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
} else {
bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE);
bind.emptyDescriptionImageView.setVisibility(View.VISIBLE);
bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE);
}
}
});
}
@SuppressLint("DefaultLocale")
private void setSyncLirics(LyricsList lyricsList) {
if (lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
StringBuilder lyricsBuilder = new StringBuilder();
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
if (lines != null) {
for (Line line : lines) {
lyricsBuilder.append(line.getValue().trim()).append("\n");
}
}
bind.nowPlayingSongLyricsTextView.setText(lyricsBuilder.toString());
}
}
private void defineProgressHandler() {
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
if (lyricsList != null) {
if (lyricsList.getStructuredLyrics() != null && lyricsList.getStructuredLyrics().get(0) != null && !lyricsList.getStructuredLyrics().get(0).getSynced()) {
releaseHandler();
return;
}
syncLyricsHandler = new Handler();
syncLyricsRunnable = () -> {
if (syncLyricsHandler != null) {
if (bind != null) {
displaySyncedLyrics();
}
syncLyricsHandler.postDelayed(syncLyricsRunnable, 250);
}
};
syncLyricsHandler.postDelayed(syncLyricsRunnable, 250);
} else {
releaseHandler();
}
});
}
private void displaySyncedLyrics() {
LyricsList lyricsList = playerBottomSheetViewModel.getLiveLyricsList().getValue();
int timestamp = (int) (mediaBrowser.getCurrentPosition());
if (lyricsList != null && lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
StringBuilder lyricsBuilder = new StringBuilder();
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
if (lines == null || lines.isEmpty()) return;
for (Line line : lines) {
lyricsBuilder.append(line.getValue().trim()).append("\n");
}
Line toHighlight = lines.stream().filter(line -> line != null && line.getStart() != null && line.getStart() < timestamp).reduce((first, second) -> second).orElse(null);
if (toHighlight != null) {
String lyrics = lyricsBuilder.toString();
Spannable spannableString = new SpannableString(lyrics);
int startingPosition = getStartPosition(lines, toHighlight);
int endingPosition = startingPosition + toHighlight.getValue().length();
spannableString.setSpan(new ForegroundColorSpan(requireContext().getResources().getColor(R.color.shadowsLyricsTextColor, null)), 0, lyrics.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spannableString.setSpan(new ForegroundColorSpan(requireContext().getResources().getColor(R.color.lyricsTextColor, null)), startingPosition, endingPosition, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
bind.nowPlayingSongLyricsTextView.setText(spannableString);
}
}
}
private int getStartPosition(List<Line> lines, Line toHighlight) {
int start = 0;
for (Line line : lines) {
if (line != toHighlight) {
start = start + line.getValue().length() + 1;
} else {
break;
}
}
return start;
}
}

View file

@ -16,15 +16,18 @@ import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.model.Queue;
import com.cappielloantonio.tempo.repository.ArtistRepository;
import com.cappielloantonio.tempo.repository.FavoriteRepository;
import com.cappielloantonio.tempo.repository.OpenRepository;
import com.cappielloantonio.tempo.repository.QueueRepository;
import com.cappielloantonio.tempo.repository.SongRepository;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.LyricsList;
import com.cappielloantonio.tempo.subsonic.models.PlayQueue;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.NetworkUtil;
import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil;
import com.cappielloantonio.tempo.util.Preferences;
import java.util.Collections;
@ -40,10 +43,10 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
private final ArtistRepository artistRepository;
private final QueueRepository queueRepository;
private final FavoriteRepository favoriteRepository;
private final OpenRepository openRepository;
private final MutableLiveData<String> lyricsLiveData = new MutableLiveData<>(null);
private final MutableLiveData<LyricsList> lyricsListLiveData = new MutableLiveData<>(null);
private final MutableLiveData<String> descriptionLiveData = new MutableLiveData<>(null);
private final MutableLiveData<Child> liveMedia = new MutableLiveData<>(null);
private final MutableLiveData<ArtistID3> liveArtist = new MutableLiveData<>(null);
private final MutableLiveData<List<Child>> instantMix = new MutableLiveData<>(null);
@ -56,6 +59,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
artistRepository = new ArtistRepository();
queueRepository = new QueueRepository();
favoriteRepository = new FavoriteRepository();
openRepository = new OpenRepository();
}
public LiveData<List<Queue>> getQueueSong() {
@ -125,8 +129,18 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
return lyricsLiveData;
}
public LiveData<LyricsList> getLiveLyricsList() {
return lyricsListLiveData;
}
public void refreshMediaInfo(LifecycleOwner owner, Child media) {
songRepository.getSongLyrics(media).observe(owner, lyricsLiveData::postValue);
if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) {
openRepository.getLyricsBySongId(media.getId()).observe(owner, lyricsListLiveData::postValue);
lyricsLiveData.postValue(null);
} else {
songRepository.getSongLyrics(media).observe(owner, lyricsLiveData::postValue);
lyricsListLiveData.postValue(null);
}
}
public LiveData<Child> getLiveMedia() {

View file

@ -32,6 +32,7 @@
app:layout_constraintTop_toBottomOf="@+id/empty_description_image_view" />
<androidx.core.widget.NestedScrollView
android:id="@+id/now_playing_song_lyrics_sroll_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"

View file

@ -4,6 +4,9 @@
<color name="subtitleTextColor">#9B9B9B</color>
<color name="dividerColor">#404040</color>
<color name="lyricsTextColor">#DADADA</color>
<color name="shadowsLyricsTextColor">#606060</color>
<color name="searchPlaceholderColor">#CFCFCF</color>
<color name="searchColor">#DADADA</color>
</resources>

View file

@ -61,6 +61,9 @@
<color name="dividerColor">#e0e0e0</color>
<color name="white">#FFFFFF</color>
<color name="lyricsTextColor">#252525</color>
<color name="shadowsLyricsTextColor">#B4B4B4</color>
<color name="searchPlaceholderColor">#303030</color>
<color name="searchColor">#252525</color>