mirror of
https://github.com/antebudimir/tempus.git
synced 2025-12-31 09:33:33 +00:00
feat: Enable downloading of song lyrics for offline viewing
This commit is contained in:
parent
8bb6c02e46
commit
c2b6d7eed5
12 changed files with 1652 additions and 67 deletions
1151
app/schemas/com.cappielloantonio.tempo.database.AppDatabase/12.json
Normal file
1151
app/schemas/com.cappielloantonio.tempo.database.AppDatabase/12.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -12,6 +12,7 @@ import com.cappielloantonio.tempo.database.converter.DateConverters;
|
||||||
import com.cappielloantonio.tempo.database.dao.ChronologyDao;
|
import com.cappielloantonio.tempo.database.dao.ChronologyDao;
|
||||||
import com.cappielloantonio.tempo.database.dao.DownloadDao;
|
import com.cappielloantonio.tempo.database.dao.DownloadDao;
|
||||||
import com.cappielloantonio.tempo.database.dao.FavoriteDao;
|
import com.cappielloantonio.tempo.database.dao.FavoriteDao;
|
||||||
|
import com.cappielloantonio.tempo.database.dao.LyricsDao;
|
||||||
import com.cappielloantonio.tempo.database.dao.PlaylistDao;
|
import com.cappielloantonio.tempo.database.dao.PlaylistDao;
|
||||||
import com.cappielloantonio.tempo.database.dao.QueueDao;
|
import com.cappielloantonio.tempo.database.dao.QueueDao;
|
||||||
import com.cappielloantonio.tempo.database.dao.RecentSearchDao;
|
import com.cappielloantonio.tempo.database.dao.RecentSearchDao;
|
||||||
|
|
@ -20,6 +21,7 @@ import com.cappielloantonio.tempo.database.dao.SessionMediaItemDao;
|
||||||
import com.cappielloantonio.tempo.model.Chronology;
|
import com.cappielloantonio.tempo.model.Chronology;
|
||||||
import com.cappielloantonio.tempo.model.Download;
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
import com.cappielloantonio.tempo.model.Favorite;
|
import com.cappielloantonio.tempo.model.Favorite;
|
||||||
|
import com.cappielloantonio.tempo.model.LyricsCache;
|
||||||
import com.cappielloantonio.tempo.model.Queue;
|
import com.cappielloantonio.tempo.model.Queue;
|
||||||
import com.cappielloantonio.tempo.model.RecentSearch;
|
import com.cappielloantonio.tempo.model.RecentSearch;
|
||||||
import com.cappielloantonio.tempo.model.Server;
|
import com.cappielloantonio.tempo.model.Server;
|
||||||
|
|
@ -28,9 +30,9 @@ import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@Database(
|
@Database(
|
||||||
version = 11,
|
version = 12,
|
||||||
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class},
|
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class, LyricsCache.class},
|
||||||
autoMigrations = {@AutoMigration(from = 10, to = 11)}
|
autoMigrations = {@AutoMigration(from = 10, to = 11), @AutoMigration(from = 11, to = 12)}
|
||||||
)
|
)
|
||||||
@TypeConverters({DateConverters.class})
|
@TypeConverters({DateConverters.class})
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|
@ -62,4 +64,6 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
public abstract SessionMediaItemDao sessionMediaItemDao();
|
public abstract SessionMediaItemDao sessionMediaItemDao();
|
||||||
|
|
||||||
public abstract PlaylistDao playlistDao();
|
public abstract PlaylistDao playlistDao();
|
||||||
|
|
||||||
|
public abstract LyricsDao lyricsDao();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.cappielloantonio.tempo.database.dao;
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.room.Dao;
|
||||||
|
import androidx.room.Insert;
|
||||||
|
import androidx.room.OnConflictStrategy;
|
||||||
|
import androidx.room.Query;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.model.LyricsCache;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public interface LyricsDao {
|
||||||
|
@Query("SELECT * FROM lyrics_cache WHERE song_id = :songId")
|
||||||
|
LyricsCache getOne(String songId);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM lyrics_cache WHERE song_id = :songId")
|
||||||
|
LiveData<LyricsCache> observeOne(String songId);
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
void insert(LyricsCache lyricsCache);
|
||||||
|
|
||||||
|
@Query("DELETE FROM lyrics_cache WHERE song_id = :songId")
|
||||||
|
void delete(String songId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.cappielloantonio.tempo.model
|
||||||
|
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import kotlin.jvm.JvmOverloads
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@Entity(tableName = "lyrics_cache")
|
||||||
|
data class LyricsCache @JvmOverloads constructor(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "song_id")
|
||||||
|
var songId: String,
|
||||||
|
@ColumnInfo(name = "artist")
|
||||||
|
var artist: String? = null,
|
||||||
|
@ColumnInfo(name = "title")
|
||||||
|
var title: String? = null,
|
||||||
|
@ColumnInfo(name = "lyrics")
|
||||||
|
var lyrics: String? = null,
|
||||||
|
@ColumnInfo(name = "structured_lyrics")
|
||||||
|
var structuredLyrics: String? = null,
|
||||||
|
@ColumnInfo(name = "updated_at")
|
||||||
|
var updatedAt: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
package com.cappielloantonio.tempo.repository;
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.database.AppDatabase;
|
||||||
|
import com.cappielloantonio.tempo.database.dao.LyricsDao;
|
||||||
|
import com.cappielloantonio.tempo.model.LyricsCache;
|
||||||
|
|
||||||
|
public class LyricsRepository {
|
||||||
|
private final LyricsDao lyricsDao = AppDatabase.getInstance().lyricsDao();
|
||||||
|
|
||||||
|
public LyricsCache getLyrics(String songId) {
|
||||||
|
GetLyricsThreadSafe getLyricsThreadSafe = new GetLyricsThreadSafe(lyricsDao, songId);
|
||||||
|
Thread thread = new Thread(getLyricsThreadSafe);
|
||||||
|
thread.start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
thread.join();
|
||||||
|
return getLyricsThreadSafe.getLyrics();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<LyricsCache> observeLyrics(String songId) {
|
||||||
|
return lyricsDao.observeOne(songId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void insert(LyricsCache lyricsCache) {
|
||||||
|
InsertThreadSafe insert = new InsertThreadSafe(lyricsDao, lyricsCache);
|
||||||
|
Thread thread = new Thread(insert);
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(String songId) {
|
||||||
|
DeleteThreadSafe delete = new DeleteThreadSafe(lyricsDao, songId);
|
||||||
|
Thread thread = new Thread(delete);
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class GetLyricsThreadSafe implements Runnable {
|
||||||
|
private final LyricsDao lyricsDao;
|
||||||
|
private final String songId;
|
||||||
|
private LyricsCache lyricsCache;
|
||||||
|
|
||||||
|
public GetLyricsThreadSafe(LyricsDao lyricsDao, String songId) {
|
||||||
|
this.lyricsDao = lyricsDao;
|
||||||
|
this.songId = songId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
lyricsCache = lyricsDao.getOne(songId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LyricsCache getLyrics() {
|
||||||
|
return lyricsCache;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class InsertThreadSafe implements Runnable {
|
||||||
|
private final LyricsDao lyricsDao;
|
||||||
|
private final LyricsCache lyricsCache;
|
||||||
|
|
||||||
|
public InsertThreadSafe(LyricsDao lyricsDao, LyricsCache lyricsCache) {
|
||||||
|
this.lyricsDao = lyricsDao;
|
||||||
|
this.lyricsCache = lyricsCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
lyricsDao.insert(lyricsCache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DeleteThreadSafe implements Runnable {
|
||||||
|
private final LyricsDao lyricsDao;
|
||||||
|
private final String songId;
|
||||||
|
|
||||||
|
public DeleteThreadSafe(LyricsDao lyricsDao, String songId) {
|
||||||
|
this.lyricsDao = lyricsDao;
|
||||||
|
this.songId = songId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
lyricsDao.delete(songId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,15 +4,16 @@ import android.annotation.SuppressLint;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.text.Layout;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.Layout;
|
import android.text.TextUtils;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
@ -29,10 +30,10 @@ import com.cappielloantonio.tempo.service.MediaService;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Line;
|
import com.cappielloantonio.tempo.subsonic.models.Line;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.LyricsList;
|
import com.cappielloantonio.tempo.subsonic.models.LyricsList;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil;
|
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.android.material.button.MaterialButton;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -48,6 +49,9 @@ public class PlayerLyricsFragment extends Fragment {
|
||||||
private MediaBrowser mediaBrowser;
|
private MediaBrowser mediaBrowser;
|
||||||
private Handler syncLyricsHandler;
|
private Handler syncLyricsHandler;
|
||||||
private Runnable syncLyricsRunnable;
|
private Runnable syncLyricsRunnable;
|
||||||
|
private String currentLyrics;
|
||||||
|
private LyricsList currentLyricsList;
|
||||||
|
private String currentDescription;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
|
@ -66,6 +70,7 @@ public class PlayerLyricsFragment extends Fragment {
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
|
||||||
initPanelContent();
|
initPanelContent();
|
||||||
|
observeDownloadState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -101,12 +106,26 @@ public class PlayerLyricsFragment extends Fragment {
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
bind = null;
|
bind = null;
|
||||||
|
currentLyrics = null;
|
||||||
|
currentLyricsList = null;
|
||||||
|
currentDescription = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initOverlay() {
|
private void initOverlay() {
|
||||||
bind.syncLyricsTapButton.setOnClickListener(view -> {
|
bind.syncLyricsTapButton.setOnClickListener(view -> {
|
||||||
playerBottomSheetViewModel.changeSyncLyricsState();
|
playerBottomSheetViewModel.changeSyncLyricsState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bind.downloadLyricsButton.setOnClickListener(view -> {
|
||||||
|
boolean saved = playerBottomSheetViewModel.downloadCurrentLyrics();
|
||||||
|
if (getContext() != null) {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
saved ? R.string.player_lyrics_download_success : R.string.player_lyrics_download_failure,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeBrowser() {
|
private void initializeBrowser() {
|
||||||
|
|
@ -136,50 +155,91 @@ public class PlayerLyricsFragment extends Fragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initPanelContent() {
|
private void initPanelContent() {
|
||||||
if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) {
|
playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> {
|
||||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
currentLyrics = lyrics;
|
||||||
setPanelContent(null, lyricsList);
|
updatePanelContent();
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> {
|
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||||
setPanelContent(lyrics, null);
|
currentLyricsList = lyricsList;
|
||||||
});
|
updatePanelContent();
|
||||||
}
|
});
|
||||||
|
|
||||||
|
playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> {
|
||||||
|
currentDescription = description;
|
||||||
|
updatePanelContent();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setPanelContent(String lyrics, LyricsList lyricsList) {
|
private void observeDownloadState() {
|
||||||
playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> {
|
playerBottomSheetViewModel.getLyricsCachedState().observe(getViewLifecycleOwner(), cached -> {
|
||||||
if (bind != null) {
|
if (bind != null) {
|
||||||
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
|
MaterialButton downloadButton = (MaterialButton) bind.downloadLyricsButton;
|
||||||
|
if (cached != null && cached) {
|
||||||
if (lyrics != null && !lyrics.trim().equals("")) {
|
downloadButton.setIconResource(R.drawable.ic_done);
|
||||||
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(lyrics));
|
downloadButton.setContentDescription(getString(R.string.player_lyrics_downloaded_content_description));
|
||||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
|
||||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
|
||||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
|
||||||
bind.syncLyricsTapButton.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);
|
|
||||||
bind.syncLyricsTapButton.setVisibility(View.VISIBLE);
|
|
||||||
} 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);
|
|
||||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
|
||||||
} else {
|
} else {
|
||||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE);
|
downloadButton.setIconResource(R.drawable.ic_download);
|
||||||
bind.emptyDescriptionImageView.setVisibility(View.VISIBLE);
|
downloadButton.setContentDescription(getString(R.string.player_lyrics_download_content_description));
|
||||||
bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE);
|
|
||||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updatePanelContent() {
|
||||||
|
if (bind == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
|
||||||
|
|
||||||
|
if (hasStructuredLyrics(currentLyricsList)) {
|
||||||
|
setSyncLirics(currentLyricsList);
|
||||||
|
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||||
|
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||||
|
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||||
|
bind.syncLyricsTapButton.setVisibility(View.VISIBLE);
|
||||||
|
bind.downloadLyricsButton.setVisibility(View.VISIBLE);
|
||||||
|
bind.downloadLyricsButton.setEnabled(true);
|
||||||
|
} else if (hasText(currentLyrics)) {
|
||||||
|
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(currentLyrics));
|
||||||
|
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||||
|
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||||
|
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||||
|
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||||
|
bind.downloadLyricsButton.setVisibility(View.VISIBLE);
|
||||||
|
bind.downloadLyricsButton.setEnabled(true);
|
||||||
|
} else if (hasText(currentDescription)) {
|
||||||
|
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(currentDescription));
|
||||||
|
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||||
|
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||||
|
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||||
|
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||||
|
bind.downloadLyricsButton.setVisibility(View.GONE);
|
||||||
|
bind.downloadLyricsButton.setEnabled(false);
|
||||||
|
} else {
|
||||||
|
bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE);
|
||||||
|
bind.emptyDescriptionImageView.setVisibility(View.VISIBLE);
|
||||||
|
bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE);
|
||||||
|
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||||
|
bind.downloadLyricsButton.setVisibility(View.GONE);
|
||||||
|
bind.downloadLyricsButton.setEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasText(String value) {
|
||||||
|
return value != null && !value.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasStructuredLyrics(LyricsList lyricsList) {
|
||||||
|
return lyricsList != null
|
||||||
|
&& lyricsList.getStructuredLyrics() != null
|
||||||
|
&& !lyricsList.getStructuredLyrics().isEmpty()
|
||||||
|
&& lyricsList.getStructuredLyrics().get(0) != null
|
||||||
|
&& lyricsList.getStructuredLyrics().get(0).getLine() != null
|
||||||
|
&& !lyricsList.getStructuredLyrics().get(0).getLine().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@SuppressLint("DefaultLocale")
|
||||||
private void setSyncLirics(LyricsList lyricsList) {
|
private void setSyncLirics(LyricsList lyricsList) {
|
||||||
if (lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
|
if (lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
|
||||||
|
|
@ -198,28 +258,28 @@ public class PlayerLyricsFragment extends Fragment {
|
||||||
|
|
||||||
private void defineProgressHandler() {
|
private void defineProgressHandler() {
|
||||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||||
if (lyricsList != null) {
|
if (!hasStructuredLyrics(lyricsList)) {
|
||||||
|
|
||||||
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();
|
releaseHandler();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,7 +287,7 @@ public class PlayerLyricsFragment extends Fragment {
|
||||||
LyricsList lyricsList = playerBottomSheetViewModel.getLiveLyricsList().getValue();
|
LyricsList lyricsList = playerBottomSheetViewModel.getLiveLyricsList().getValue();
|
||||||
int timestamp = (int) (mediaBrowser.getCurrentPosition());
|
int timestamp = (int) (mediaBrowser.getCurrentPosition());
|
||||||
|
|
||||||
if (lyricsList != null && lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
|
if (hasStructuredLyrics(lyricsList)) {
|
||||||
StringBuilder lyricsBuilder = new StringBuilder();
|
StringBuilder lyricsBuilder = new StringBuilder();
|
||||||
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
actionChangeDownloadStorage();
|
actionChangeDownloadStorage();
|
||||||
actionDeleteDownloadStorage();
|
actionDeleteDownloadStorage();
|
||||||
actionKeepScreenOn();
|
actionKeepScreenOn();
|
||||||
|
actionAutoDownloadLyrics();
|
||||||
|
|
||||||
bindMediaService();
|
bindMediaService();
|
||||||
actionAppEqualizer();
|
actionAppEqualizer();
|
||||||
|
|
@ -357,6 +358,21 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void actionAutoDownloadLyrics() {
|
||||||
|
SwitchPreference preference = findPreference("auto_download_lyrics");
|
||||||
|
if (preference == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
preference.setChecked(Preferences.isAutoDownloadLyricsEnabled());
|
||||||
|
preference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||||
|
if (newValue instanceof Boolean) {
|
||||||
|
Preferences.setAutoDownloadLyricsEnabled((Boolean) newValue);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void getScanStatus() {
|
private void getScanStatus() {
|
||||||
settingViewModel.getScanStatus(new ScanCallback() {
|
settingViewModel.getScanStatus(new ScanCallback() {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ object Preferences {
|
||||||
private const val ROUNDED_CORNER_SIZE = "rounded_corner_size"
|
private const val ROUNDED_CORNER_SIZE = "rounded_corner_size"
|
||||||
private const val PODCAST_SECTION_VISIBILITY = "podcast_section_visibility"
|
private const val PODCAST_SECTION_VISIBILITY = "podcast_section_visibility"
|
||||||
private const val RADIO_SECTION_VISIBILITY = "radio_section_visibility"
|
private const val RADIO_SECTION_VISIBILITY = "radio_section_visibility"
|
||||||
|
private const val AUTO_DOWNLOAD_LYRICS = "auto_download_lyrics"
|
||||||
private const val MUSIC_DIRECTORY_SECTION_VISIBILITY = "music_directory_section_visibility"
|
private const val MUSIC_DIRECTORY_SECTION_VISIBILITY = "music_directory_section_visibility"
|
||||||
private const val REPLAY_GAIN_MODE = "replay_gain_mode"
|
private const val REPLAY_GAIN_MODE = "replay_gain_mode"
|
||||||
private const val AUDIO_TRANSCODE_PRIORITY = "audio_transcode_priority"
|
private const val AUDIO_TRANSCODE_PRIORITY = "audio_transcode_priority"
|
||||||
|
|
@ -163,6 +164,24 @@ object Preferences {
|
||||||
App.getInstance().preferences.edit().putString(OPEN_SUBSONIC_EXTENSIONS, Gson().toJson(extension)).apply()
|
App.getInstance().preferences.edit().putString(OPEN_SUBSONIC_EXTENSIONS, Gson().toJson(extension)).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isAutoDownloadLyricsEnabled(): Boolean {
|
||||||
|
val preferences = App.getInstance().preferences
|
||||||
|
|
||||||
|
if (preferences.contains(AUTO_DOWNLOAD_LYRICS)) {
|
||||||
|
return preferences.getBoolean(AUTO_DOWNLOAD_LYRICS, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setAutoDownloadLyricsEnabled(isEnabled: Boolean) {
|
||||||
|
App.getInstance().preferences.edit()
|
||||||
|
.putBoolean(AUTO_DOWNLOAD_LYRICS, isEnabled)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getLocalAddress(): String? {
|
fun getLocalAddress(): String? {
|
||||||
return App.getInstance().preferences.getString(LOCAL_ADDRESS, null)
|
return App.getInstance().preferences.getString(LOCAL_ADDRESS, null)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.viewmodel;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.OptIn;
|
import androidx.annotation.OptIn;
|
||||||
|
|
@ -9,14 +10,17 @@ import androidx.lifecycle.AndroidViewModel;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
||||||
import com.cappielloantonio.tempo.model.Download;
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
|
import com.cappielloantonio.tempo.model.LyricsCache;
|
||||||
import com.cappielloantonio.tempo.model.Queue;
|
import com.cappielloantonio.tempo.model.Queue;
|
||||||
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||||
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
||||||
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
||||||
|
import com.cappielloantonio.tempo.repository.LyricsRepository;
|
||||||
import com.cappielloantonio.tempo.repository.OpenRepository;
|
import com.cappielloantonio.tempo.repository.OpenRepository;
|
||||||
import com.cappielloantonio.tempo.repository.QueueRepository;
|
import com.cappielloantonio.tempo.repository.QueueRepository;
|
||||||
import com.cappielloantonio.tempo.repository.SongRepository;
|
import com.cappielloantonio.tempo.repository.SongRepository;
|
||||||
|
|
@ -31,6 +35,7 @@ import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.NetworkUtil;
|
import com.cappielloantonio.tempo.util.NetworkUtil;
|
||||||
import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil;
|
import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
@ -47,14 +52,20 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
||||||
private final QueueRepository queueRepository;
|
private final QueueRepository queueRepository;
|
||||||
private final FavoriteRepository favoriteRepository;
|
private final FavoriteRepository favoriteRepository;
|
||||||
private final OpenRepository openRepository;
|
private final OpenRepository openRepository;
|
||||||
|
private final LyricsRepository lyricsRepository;
|
||||||
private final MutableLiveData<String> lyricsLiveData = new MutableLiveData<>(null);
|
private final MutableLiveData<String> lyricsLiveData = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<LyricsList> lyricsListLiveData = new MutableLiveData<>(null);
|
private final MutableLiveData<LyricsList> lyricsListLiveData = new MutableLiveData<>(null);
|
||||||
|
private final MutableLiveData<Boolean> lyricsCachedLiveData = new MutableLiveData<>(false);
|
||||||
private final MutableLiveData<String> descriptionLiveData = new MutableLiveData<>(null);
|
private final MutableLiveData<String> descriptionLiveData = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<Child> liveMedia = new MutableLiveData<>(null);
|
private final MutableLiveData<Child> liveMedia = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<AlbumID3> liveAlbum = new MutableLiveData<>(null);
|
private final MutableLiveData<AlbumID3> liveAlbum = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<ArtistID3> liveArtist = new MutableLiveData<>(null);
|
private final MutableLiveData<ArtistID3> liveArtist = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<List<Child>> instantMix = new MutableLiveData<>(null);
|
private final MutableLiveData<List<Child>> instantMix = new MutableLiveData<>(null);
|
||||||
|
private final Gson gson = new Gson();
|
||||||
private boolean lyricsSyncState = true;
|
private boolean lyricsSyncState = true;
|
||||||
|
private LiveData<LyricsCache> cachedLyricsSource;
|
||||||
|
private String currentSongId;
|
||||||
|
private final Observer<LyricsCache> cachedLyricsObserver = this::onCachedLyricsChanged;
|
||||||
|
|
||||||
|
|
||||||
public PlayerBottomSheetViewModel(@NonNull Application application) {
|
public PlayerBottomSheetViewModel(@NonNull Application application) {
|
||||||
|
|
@ -66,6 +77,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
||||||
queueRepository = new QueueRepository();
|
queueRepository = new QueueRepository();
|
||||||
favoriteRepository = new FavoriteRepository();
|
favoriteRepository = new FavoriteRepository();
|
||||||
openRepository = new OpenRepository();
|
openRepository = new OpenRepository();
|
||||||
|
lyricsRepository = new LyricsRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<List<Queue>> getQueueSong() {
|
public LiveData<List<Queue>> getQueueSong() {
|
||||||
|
|
@ -139,12 +151,49 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refreshMediaInfo(LifecycleOwner owner, Child media) {
|
public void refreshMediaInfo(LifecycleOwner owner, Child media) {
|
||||||
|
lyricsLiveData.postValue(null);
|
||||||
|
lyricsListLiveData.postValue(null);
|
||||||
|
lyricsCachedLiveData.postValue(false);
|
||||||
|
|
||||||
|
clearCachedLyricsObserver();
|
||||||
|
|
||||||
|
String songId = media != null ? media.getId() : currentSongId;
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(songId) || owner == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSongId = songId;
|
||||||
|
|
||||||
|
observeCachedLyrics(owner, songId);
|
||||||
|
|
||||||
|
LyricsCache cachedLyrics = lyricsRepository.getLyrics(songId);
|
||||||
|
if (cachedLyrics != null) {
|
||||||
|
onCachedLyricsChanged(cachedLyrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NetworkUtil.isOffline() || media == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) {
|
if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) {
|
||||||
openRepository.getLyricsBySongId(media.getId()).observe(owner, lyricsListLiveData::postValue);
|
openRepository.getLyricsBySongId(media.getId()).observe(owner, lyricsList -> {
|
||||||
lyricsLiveData.postValue(null);
|
lyricsListLiveData.postValue(lyricsList);
|
||||||
|
lyricsLiveData.postValue(null);
|
||||||
|
|
||||||
|
if (shouldAutoDownloadLyrics() && hasStructuredLyrics(lyricsList)) {
|
||||||
|
saveLyricsToCache(media, null, lyricsList);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
songRepository.getSongLyrics(media).observe(owner, lyricsLiveData::postValue);
|
songRepository.getSongLyrics(media).observe(owner, lyrics -> {
|
||||||
lyricsListLiveData.postValue(null);
|
lyricsLiveData.postValue(lyrics);
|
||||||
|
lyricsListLiveData.postValue(null);
|
||||||
|
|
||||||
|
if (shouldAutoDownloadLyrics() && !TextUtils.isEmpty(lyrics)) {
|
||||||
|
saveLyricsToCache(media, lyrics, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,6 +202,17 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLiveMedia(LifecycleOwner owner, String mediaType, String mediaId) {
|
public void setLiveMedia(LifecycleOwner owner, String mediaType, String mediaId) {
|
||||||
|
currentSongId = mediaId;
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(mediaId)) {
|
||||||
|
refreshMediaInfo(owner, null);
|
||||||
|
} else {
|
||||||
|
clearCachedLyricsObserver();
|
||||||
|
lyricsLiveData.postValue(null);
|
||||||
|
lyricsListLiveData.postValue(null);
|
||||||
|
lyricsCachedLiveData.postValue(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (mediaType != null) {
|
if (mediaType != null) {
|
||||||
switch (mediaType) {
|
switch (mediaType) {
|
||||||
case Constants.MEDIA_TYPE_MUSIC:
|
case Constants.MEDIA_TYPE_MUSIC:
|
||||||
|
|
@ -162,7 +222,12 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
||||||
case Constants.MEDIA_TYPE_PODCAST:
|
case Constants.MEDIA_TYPE_PODCAST:
|
||||||
liveMedia.postValue(null);
|
liveMedia.postValue(null);
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
liveMedia.postValue(null);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
liveMedia.postValue(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,6 +298,105 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void observeCachedLyrics(LifecycleOwner owner, String songId) {
|
||||||
|
if (TextUtils.isEmpty(songId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedLyricsSource = lyricsRepository.observeLyrics(songId);
|
||||||
|
cachedLyricsSource.observe(owner, cachedLyricsObserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearCachedLyricsObserver() {
|
||||||
|
if (cachedLyricsSource != null) {
|
||||||
|
cachedLyricsSource.removeObserver(cachedLyricsObserver);
|
||||||
|
cachedLyricsSource = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onCachedLyricsChanged(LyricsCache lyricsCache) {
|
||||||
|
if (lyricsCache == null) {
|
||||||
|
lyricsCachedLiveData.postValue(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lyricsCachedLiveData.postValue(true);
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(lyricsCache.getStructuredLyrics())) {
|
||||||
|
try {
|
||||||
|
LyricsList cachedList = gson.fromJson(lyricsCache.getStructuredLyrics(), LyricsList.class);
|
||||||
|
lyricsListLiveData.postValue(cachedList);
|
||||||
|
lyricsLiveData.postValue(null);
|
||||||
|
} catch (Exception exception) {
|
||||||
|
lyricsListLiveData.postValue(null);
|
||||||
|
lyricsLiveData.postValue(lyricsCache.getLyrics());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lyricsListLiveData.postValue(null);
|
||||||
|
lyricsLiveData.postValue(lyricsCache.getLyrics());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveLyricsToCache(Child media, String lyrics, LyricsList lyricsList) {
|
||||||
|
if (media == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((lyricsList == null || !hasStructuredLyrics(lyricsList)) && TextUtils.isEmpty(lyrics)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LyricsCache lyricsCache = new LyricsCache(media.getId());
|
||||||
|
lyricsCache.setArtist(media.getArtist());
|
||||||
|
lyricsCache.setTitle(media.getTitle());
|
||||||
|
lyricsCache.setUpdatedAt(System.currentTimeMillis());
|
||||||
|
|
||||||
|
if (lyricsList != null && hasStructuredLyrics(lyricsList)) {
|
||||||
|
lyricsCache.setStructuredLyrics(gson.toJson(lyricsList));
|
||||||
|
lyricsCache.setLyrics(null);
|
||||||
|
} else {
|
||||||
|
lyricsCache.setLyrics(lyrics);
|
||||||
|
lyricsCache.setStructuredLyrics(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
lyricsRepository.insert(lyricsCache);
|
||||||
|
lyricsCachedLiveData.postValue(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasStructuredLyrics(LyricsList lyricsList) {
|
||||||
|
return lyricsList != null
|
||||||
|
&& lyricsList.getStructuredLyrics() != null
|
||||||
|
&& !lyricsList.getStructuredLyrics().isEmpty()
|
||||||
|
&& lyricsList.getStructuredLyrics().get(0) != null
|
||||||
|
&& lyricsList.getStructuredLyrics().get(0).getLine() != null
|
||||||
|
&& !lyricsList.getStructuredLyrics().get(0).getLine().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldAutoDownloadLyrics() {
|
||||||
|
return Preferences.isAutoDownloadLyricsEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean downloadCurrentLyrics() {
|
||||||
|
Child media = getLiveMedia().getValue();
|
||||||
|
if (media == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LyricsList lyricsList = lyricsListLiveData.getValue();
|
||||||
|
String lyrics = lyricsLiveData.getValue();
|
||||||
|
|
||||||
|
if ((lyricsList == null || !hasStructuredLyrics(lyricsList)) && TextUtils.isEmpty(lyrics)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveLyricsToCache(media, lyrics, lyricsList);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<Boolean> getLyricsCachedState() {
|
||||||
|
return lyricsCachedLiveData;
|
||||||
|
}
|
||||||
|
|
||||||
public void changeSyncLyricsState() {
|
public void changeSyncLyricsState() {
|
||||||
lyricsSyncState = !lyricsSyncState;
|
lyricsSyncState = !lyricsSyncState;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,25 @@
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
<Button
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/download_lyrics_button"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:alpha="0.7"
|
||||||
|
android:contentDescription="@string/player_lyrics_download_content_description"
|
||||||
|
android:insetLeft="0dp"
|
||||||
|
android:insetTop="0dp"
|
||||||
|
android:insetRight="0dp"
|
||||||
|
android:insetBottom="0dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:cornerRadius="64dp"
|
||||||
|
app:icon="@drawable/ic_download"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/sync_lyrics_tap_button"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/now_playing_song_lyrics_sroll_view" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/sync_lyrics_tap_button"
|
android:id="@+id/sync_lyrics_tap_button"
|
||||||
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
||||||
android:layout_width="48dp"
|
android:layout_width="48dp"
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,10 @@
|
||||||
<string name="player_playback_speed">%1$.2fx</string>
|
<string name="player_playback_speed">%1$.2fx</string>
|
||||||
<string name="player_queue_clean_all_button">Clean play queue</string>
|
<string name="player_queue_clean_all_button">Clean play queue</string>
|
||||||
<string name="player_queue_save_queue_success">Saved play queue</string>
|
<string name="player_queue_save_queue_success">Saved play queue</string>
|
||||||
|
<string name="player_lyrics_download_content_description">Download lyrics for offline playback</string>
|
||||||
|
<string name="player_lyrics_downloaded_content_description">Lyrics downloaded for offline playback</string>
|
||||||
|
<string name="player_lyrics_download_success">Lyrics saved for offline playback.</string>
|
||||||
|
<string name="player_lyrics_download_failure">Lyrics are not available to download.</string>
|
||||||
<string name="player_server_priority">Server Priority</string>
|
<string name="player_server_priority">Server Priority</string>
|
||||||
<string name="player_unknown_format">Unknown format</string>
|
<string name="player_unknown_format">Unknown format</string>
|
||||||
<string name="player_transcoding">Transcoding</string>
|
<string name="player_transcoding">Transcoding</string>
|
||||||
|
|
@ -327,6 +331,8 @@
|
||||||
<string name="settings_queue_syncing_title">Sync play queue for this user [Not Fully Baked]</string>
|
<string name="settings_queue_syncing_title">Sync play queue for this user [Not Fully Baked]</string>
|
||||||
<string name="settings_radio">Show radio</string>
|
<string name="settings_radio">Show radio</string>
|
||||||
<string name="settings_radio_summary">If enabled, show the radio section. Restart the app for it to take full effect.</string>
|
<string name="settings_radio_summary">If enabled, show the radio section. Restart the app for it to take full effect.</string>
|
||||||
|
<string name="settings_auto_download_lyrics">Auto download lyrics</string>
|
||||||
|
<string name="settings_auto_download_lyrics_summary">Automatically save lyrics when they are available so they can be shown while offline.</string>
|
||||||
<string name="settings_replay_gain">Set replay gain mode</string>
|
<string name="settings_replay_gain">Set replay gain mode</string>
|
||||||
<string name="settings_rounded_corner">Rounded corners</string>
|
<string name="settings_rounded_corner">Rounded corners</string>
|
||||||
<string name="settings_rounded_corner_size">Corners size</string>
|
<string name="settings_rounded_corner_size">Corners size</string>
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,12 @@
|
||||||
android:summary="@string/settings_radio_summary"
|
android:summary="@string/settings_radio_summary"
|
||||||
android:key="radio_section_visibility" />
|
android:key="radio_section_visibility" />
|
||||||
|
|
||||||
|
<SwitchPreference
|
||||||
|
android:title="@string/settings_auto_download_lyrics"
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:summary="@string/settings_auto_download_lyrics_summary"
|
||||||
|
android:key="auto_download_lyrics" />
|
||||||
|
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
android:title="@string/settings_music_directory"
|
android:title="@string/settings_music_directory"
|
||||||
android:defaultValue="true"
|
android:defaultValue="true"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue