diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java index 761c6f3c..e618ec12 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java @@ -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 = () -> { diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java index 306c8b50..2b48c1ad 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java @@ -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); + } } } }); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java index a492014e..5fe15e4f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java @@ -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 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 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 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 lines, Line toHighlight) { + int start = 0; + + for (Line line : lines) { + if (line != toHighlight) { + start = start + line.getValue().length() + 1; + } else { + break; + } + } + + return start; + } } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java index f91c4995..b2a7a88f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java @@ -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 lyricsLiveData = new MutableLiveData<>(null); + private final MutableLiveData lyricsListLiveData = new MutableLiveData<>(null); private final MutableLiveData descriptionLiveData = new MutableLiveData<>(null); - private final MutableLiveData liveMedia = new MutableLiveData<>(null); private final MutableLiveData liveArtist = new MutableLiveData<>(null); private final MutableLiveData> 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> getQueueSong() { @@ -125,8 +129,18 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel { return lyricsLiveData; } + public LiveData 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 getLiveMedia() { diff --git a/app/src/main/res/layout/inner_fragment_player_lyrics.xml b/app/src/main/res/layout/inner_fragment_player_lyrics.xml index bf1e8bc4..dbad7863 100644 --- a/app/src/main/res/layout/inner_fragment_player_lyrics.xml +++ b/app/src/main/res/layout/inner_fragment_player_lyrics.xml @@ -32,6 +32,7 @@ app:layout_constraintTop_toBottomOf="@+id/empty_description_image_view" /> #9B9B9B #404040 + #DADADA + #606060 + #CFCFCF #DADADA diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 32889236..3429de03 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -61,6 +61,9 @@ #e0e0e0 #FFFFFF + #252525 + #B4B4B4 + #303030 #252525