From d58c68e92d5666f7c679f80f2bf81b20e3e4924e Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sun, 16 Oct 2022 20:42:14 +0800 Subject: [PATCH 01/95] Fix #315 --- .../music/ui/fragments/settings/CacheSettingsFragment.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/settings/CacheSettingsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/settings/CacheSettingsFragment.kt index 607f26ccf..22cb2dbc4 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/settings/CacheSettingsFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/settings/CacheSettingsFragment.kt @@ -12,6 +12,7 @@ import androidx.preference.Preference import coil.annotation.ExperimentalCoilApi import coil.imageLoader import com.zionhuang.music.R +import com.zionhuang.music.extensions.tryOrNull import com.zionhuang.music.playback.MediaSessionConnection import com.zionhuang.music.playback.MusicService import com.zionhuang.music.ui.fragments.base.BaseSettingsFragment @@ -22,7 +23,7 @@ class CacheSettingsFragment : BaseSettingsFragment() { if (iBinder !is MusicService.MusicBinder) return findPreference(getString(R.string.pref_song_max_cache_size))?.apply { MediaSessionConnection.binder?.cache?.let { cache -> - summary = getString(R.string.size_used, Formatter.formatShortFileSize(context, cache.cacheSpace)) + summary = getString(R.string.size_used, Formatter.formatShortFileSize(context, tryOrNull { cache.cacheSpace } ?: 0)) } } } From 738546f4c73da228b33f23c6c7e9af95f7dbd6bf Mon Sep 17 00:00:00 2001 From: mohammed_9456 <41828058+Marco-9456@users.noreply.github.com> Date: Sun, 16 Oct 2022 16:45:16 +0200 Subject: [PATCH 02/95] Update README.md --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 479beab9e..3d0681fa0 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,17 @@ No ads, free, and simple. [![License](https://img.shields.io/github/license/z-huang/music)](https://www.gnu.org/licenses/gpl-3.0) [![Downloads](https://img.shields.io/github/downloads/z-huang/music/total)](https://github.com/z-huang/music/releases) -> **Note 1:** The project is currently in an unstable stage, so there should be many bugs. If you encounter one, please report by opening an issue. - -> **Note 2:** The icon of this app is temporary. It will be changed in the future. +> **Note** +> +> **1.** The project is currently in an unstable stage, so there should be many bugs. If you encounter one, please report by opening an issue. +> +> **2.** The icon of this app is temporary. It will be changed in the future. With this app, you're like getting a free music streaming service. You can listen to music from YouTube Music and build your own library. What's more, songs can be downloaded for offline playback. You can also create playlists to organize your songs. The aim of _InnerTune_ is to enable everyone to listen to music at no cost by an easy-to-use, practical and ad-free application. -⚠️ Warning: If you're in region that YouTube Music is not supported, you won't be able to use this app ***unless*** you have proxy or VPN to connect to a YTM supported region. +> **Warning** +> +>If you're in region that YouTube Music is not supported, you won't be able to use this app ***unless*** you have proxy or VPN to connect to a YTM supported region. ## Features @@ -96,4 +100,4 @@ How to get updates? #### Fastlane (App Description and Changelogs) -Follow the [fastlane instruction](https://gitlab.com/-/snippets/1895688) to add your language and create a pull request. \ No newline at end of file +Follow the [fastlane instruction](https://gitlab.com/-/snippets/1895688) to add your language and create a pull request. From 91b19ca6d783b62d9dca7c61278e0f3b8fc95f75 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 18 Oct 2022 13:08:48 +0800 Subject: [PATCH 03/95] Force update playback state when metadata changed (#321) --- .../com/zionhuang/music/playback/MediaSessionConnection.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/com/zionhuang/music/playback/MediaSessionConnection.kt b/app/src/main/java/com/zionhuang/music/playback/MediaSessionConnection.kt index 80080b897..087309e59 100644 --- a/app/src/main/java/com/zionhuang/music/playback/MediaSessionConnection.kt +++ b/app/src/main/java/com/zionhuang/music/playback/MediaSessionConnection.kt @@ -75,6 +75,10 @@ object MediaSessionConnection { override fun onMetadataChanged(metadata: MediaMetadataCompat?) { _mediaMetadata.value = metadata + // force update playback state + mediaController?.let { + _playbackState.value = it.playbackState + } } override fun onQueueChanged(queue: List) { From 75f2ef7b92722d4a5f1477b415c5de7761ccdfde Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 18 Oct 2022 13:13:40 +0800 Subject: [PATCH 04/95] Update app name string --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- README.md | 30 +++++++++---------- .../zionhuang/music/constants/Constants.kt | 4 +-- app/src/main/res/values/constants.xml | 10 ++++--- .../android/en-US/full_description.txt | 2 +- .../metadata/android/it/full_description.txt | 2 +- .../metadata/android/ja/full_description.txt | 2 +- .../metadata/android/ko/full_description.txt | 2 +- 8 files changed, 28 insertions(+), 26 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 25a0f6047..0cc94dcec 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -7,7 +7,7 @@ body: attributes: label: Checklist options: - - label: I am able to reproduce the bug with the [latest version](https://github.com/z-huang/music/releases/latest). + - label: I am able to reproduce the bug with the [latest version](https://github.com/z-huang/InnerTune/releases/latest). required: true - label: I've checked that there is no issue about this bug. required: true diff --git a/README.md b/README.md index 3d0681fa0..b40ff7395 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # InnerTune - + Make your own music library with any song from YouTube Music. No ads, free, and simple. [](https://apt.izzysoft.de/fdroid/index/apk/com.zionhuang.music) -[![Latest release](https://img.shields.io/github/v/release/z-huang/music?include_prereleases)](https://github.com/z-huang/music/releases) -[![License](https://img.shields.io/github/license/z-huang/music)](https://www.gnu.org/licenses/gpl-3.0) -[![Downloads](https://img.shields.io/github/downloads/z-huang/music/total)](https://github.com/z-huang/music/releases) +[![Latest release](https://img.shields.io/github/v/release/z-huang/InnerTune?include_prereleases)](https://github.com/z-huang/music/releases) +[![License](https://img.shields.io/github/license/z-huang/InnerTune)](https://www.gnu.org/licenses/gpl-3.0) +[![Downloads](https://img.shields.io/github/downloads/z-huang/InnerTune/total)](https://github.com/z-huang/InnerTune/releases) > **Note** > @@ -59,31 +59,31 @@ With this app, you're like getting a free music streaming service. You can liste ## Screenshots

- - - - + + + +

- - - - + + + +

## Installation You can install _InnerTune_ using the following methods: -1. Download the APK file from [GitHub Releases](https://github.com/z-huang/music/releases). +1. Download the APK file from [GitHub Releases](https://github.com/z-huang/InnerTune/releases). 2. Add [IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/com.zionhuang.music) to your F-Droid repos following the [instruction](https://apt.izzysoft.de/fdroid/index/info), and you can search for this app and receive updates. -3. If you want to have bug fixes or enjoy new features quickly, install debug version from [GitHub Action](https://github.com/z-huang/music/actions) at your own risk and remember to backup your data more frequently. +3. If you want to have bug fixes or enjoy new features quickly, install debug version from [GitHub Action](https://github.com/z-huang/InnerTune/actions) at your own risk and remember to backup your data more frequently. 4. Clone this repository and build a debug APK. How to get updates? 1. F-Droid application. -2. [GitHub](https://github.com/z-huang/music) +2. [GitHub](https://github.com/z-huang/InnerTune) ## Contribution diff --git a/app/src/main/java/com/zionhuang/music/constants/Constants.kt b/app/src/main/java/com/zionhuang/music/constants/Constants.kt index e48997806..cb424594a 100644 --- a/app/src/main/java/com/zionhuang/music/constants/Constants.kt +++ b/app/src/main/java/com/zionhuang/music/constants/Constants.kt @@ -16,8 +16,8 @@ object Constants { const val LIKED_PLAYLIST_ID = "LP_LIKED" const val DOWNLOADED_PLAYLIST_ID = "LP_DOWNLOADED" - const val GITHUB_URL = "https://github.com/z-huang/music" - const val GITHUB_ISSUE_URL = "https://github.com/z-huang/music/issues" + const val GITHUB_URL = "https://github.com/z-huang/InnerTune" + const val GITHUB_ISSUE_URL = "https://github.com/z-huang/InnerTune/issues" const val ERROR_INFO = "error_info" } \ No newline at end of file diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index a08c4f8ea..111acb1f1 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -50,6 +50,11 @@ BACKUP RESTORE + APP_VERSION + GITHUB + GitHub + z-huang/InnerTune + SONG_SORT_TYPE SONG_SORT_DESC ARTIST_SORT_TYPE @@ -58,10 +63,7 @@ ALBUM_SORT_DESC PLAYLIST_SORT_TYPE PLAYLIST_SORT_DESC - APP_VERSION - GITHUB - GitHub - z-huang/music + SHOW_LYRICS diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 218beef5c..4d14c16a9 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -2,7 +2,7 @@ With this app, you're like getting a free music streaming service. You can liste
Note: -The project is currently in an unstable stage. If you encounter bugs, please report by opening an issue on GitHub. +The project is currently in an unstable stage. If you encounter bugs, please report by opening an issue on GitHub.
Features: diff --git a/fastlane/metadata/android/it/full_description.txt b/fastlane/metadata/android/it/full_description.txt index 0b3d506de..0b21305f8 100644 --- a/fastlane/metadata/android/it/full_description.txt +++ b/fastlane/metadata/android/it/full_description.txt @@ -2,7 +2,7 @@ Con quest'app, avrai un servizio di streaming musicale gratuito. Puoi ascoltare
Nota: -Il progetto è attualmente in una fase instabile. Se trovi degli errori, per favore segnalali facendolo su GitHub. +Il progetto è attualmente in una fase instabile. Se trovi degli errori, per favore segnalali facendolo su GitHub.
Caratteristiche: diff --git a/fastlane/metadata/android/ja/full_description.txt b/fastlane/metadata/android/ja/full_description.txt index faec8ba0c..3828042ab 100644 --- a/fastlane/metadata/android/ja/full_description.txt +++ b/fastlane/metadata/android/ja/full_description.txt @@ -3,7 +3,7 @@
注意: -現在、プロジェクトはまだ安定しない段階です。もし不具合が発生したら、GitHubからissueを報告してください。 +現在、プロジェクトはまだ安定しない段階です。もし不具合が発生したら、GitHubからissueを報告してください。
機能: diff --git a/fastlane/metadata/android/ko/full_description.txt b/fastlane/metadata/android/ko/full_description.txt index ad18f4acd..0249a7647 100644 --- a/fastlane/metadata/android/ko/full_description.txt +++ b/fastlane/metadata/android/ko/full_description.txt @@ -2,7 +2,7 @@
메모: -그 프로젝트는 현재 불안정한 단계에 있습니다. 버그가 발생하면 GitHub에서 이슈를 열어 보고해주세요. +그 프로젝트는 현재 불안정한 단계에 있습니다. 버그가 발생하면 GitHub에서 이슈를 열어 보고해주세요.
특징: From 9d7203f0d4d8a5f24dd49198fbeaff867855d250 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 18 Oct 2022 20:13:57 +0800 Subject: [PATCH 05/95] Fix #329 --- .../java/com/zionhuang/music/repos/SongRepository.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt index 853acef97..5d2e2b799 100644 --- a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt +++ b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt @@ -366,6 +366,16 @@ object SongRepository : LocalRepository { songDao.update(song.copy(downloadState = STATE_NOT_DOWNLOADED)) // TODO } else { + upsert(FormatEntity( + id = song.id, + itag = format.itag, + mimeType = format.mimeType.split(";")[0], + codecs = format.mimeType.split("codecs=")[1].removeSurrounding("\""), + bitrate = format.bitrate, + sampleRate = format.audioSampleRate, + contentLength = format.contentLength!!, + loudnessDb = playerResponse.playerConfig?.audioConfig?.loudnessDb + )) songDao.update(song.copy(downloadState = STATE_DOWNLOADING)) val downloadManager = context.getSystemService()!! val req = DownloadManager.Request(format.url.toUri()) From 06797a55b496631aa7fe8208c47e540b82511045 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 18 Oct 2022 22:27:16 +0800 Subject: [PATCH 06/95] Remove CustomPlaylistFragment label --- app/src/main/res/navigation/main_navigation_graph.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/res/navigation/main_navigation_graph.xml b/app/src/main/res/navigation/main_navigation_graph.xml index 0d2130e68..23849301c 100644 --- a/app/src/main/res/navigation/main_navigation_graph.xml +++ b/app/src/main/res/navigation/main_navigation_graph.xml @@ -113,8 +113,7 @@ + android:name="com.zionhuang.music.ui.fragments.songs.CustomPlaylistFragment"> From 3acfe7519245ff629135769a8233af6a061f39cd Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Wed, 19 Oct 2022 19:54:47 +0800 Subject: [PATCH 07/95] Fix #334 --- .../innertube/models/MusicCarouselShelfRenderer.kt | 7 ++++++- .../zionhuang/innertube/models/SectionListRenderer.kt | 9 +++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/MusicCarouselShelfRenderer.kt b/innertube/src/main/java/com/zionhuang/innertube/models/MusicCarouselShelfRenderer.kt index f6936ede2..1aa680e73 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/models/MusicCarouselShelfRenderer.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/models/MusicCarouselShelfRenderer.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable @Serializable data class MusicCarouselShelfRenderer( - val header: Header, + val header: Header?, val contents: List, val itemSize: String, val numItemsPerColumn: Int?, @@ -27,6 +27,11 @@ data class MusicCarouselShelfRenderer( val thumbnail: ThumbnailRenderer?, val moreContentButton: Button?, ) + + fun toHeader() = com.zionhuang.innertube.models.Header( + title = musicCarouselShelfBasicHeaderRenderer.title.toString(), + moreNavigationEndpoint = musicCarouselShelfBasicHeaderRenderer.moreContentButton?.buttonRenderer?.navigationEndpoint + ) } @Serializable diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/SectionListRenderer.kt b/innertube/src/main/java/com/zionhuang/innertube/models/SectionListRenderer.kt index 544d0599f..98a627c92 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/models/SectionListRenderer.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/models/SectionListRenderer.kt @@ -52,13 +52,10 @@ data class SectionListRenderer( val gridRenderer: GridRenderer?, ) { fun toBaseItems(): List = when { - musicCarouselShelfRenderer != null -> listOf( - Header( - title = musicCarouselShelfRenderer.header.musicCarouselShelfBasicHeaderRenderer.title.toString(), - moreNavigationEndpoint = musicCarouselShelfRenderer.header.musicCarouselShelfBasicHeaderRenderer.moreContentButton?.buttonRenderer?.navigationEndpoint - ), + musicCarouselShelfRenderer != null -> listOfNotNull( + musicCarouselShelfRenderer.header?.toHeader(), CarouselSection( - id = musicCarouselShelfRenderer.header.musicCarouselShelfBasicHeaderRenderer.title.toString(), + id = musicCarouselShelfRenderer.header?.musicCarouselShelfBasicHeaderRenderer?.title.toString(), items = musicCarouselShelfRenderer.contents.mapNotNull { it.toBaseItem() }, numItemsPerColumn = musicCarouselShelfRenderer.numItemsPerColumn ?: 1, itemViewType = musicCarouselShelfRenderer.getViewType() From 7bbbe53a8e3e299cea85ad4c0e9e57989fb0923a Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Wed, 19 Oct 2022 22:45:12 +0800 Subject: [PATCH 08/95] Fix #323 --- .../com/zionhuang/music/ui/adapters/QueueItemAdapter.kt | 8 ++++++-- .../zionhuang/music/ui/fragments/QueueSheetFragment.kt | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/ui/adapters/QueueItemAdapter.kt b/app/src/main/java/com/zionhuang/music/ui/adapters/QueueItemAdapter.kt index b347d8dd6..cbfacbb16 100644 --- a/app/src/main/java/com/zionhuang/music/ui/adapters/QueueItemAdapter.kt +++ b/app/src/main/java/com/zionhuang/music/ui/adapters/QueueItemAdapter.kt @@ -38,6 +38,11 @@ class QueueItemAdapter(private val itemTouchHelper: ItemTouchHelper) : RecyclerV notifyItemMoved(from, to) } + fun removeItem(index: Int) { + currentList.removeAt(index) + notifyItemRemoved(index) + } + suspend fun submitData(newList: List) { val oldList: List = currentList val result = withContext(IO) { @@ -60,8 +65,7 @@ class QueueItemAdapter(private val itemTouchHelper: ItemTouchHelper) : RecyclerV override fun areItemsTheSame(oldItem: QueueItem, newItem: QueueItem): Boolean = oldItem.description.mediaId == newItem.description.mediaId override fun areContentsTheSame(oldItem: QueueItem, newItem: QueueItem): Boolean = oldItem.description.title.toString() == newItem.description.title.toString() && - oldItem.description.subtitle.toString() == newItem.description.subtitle.toString() && - oldItem.description.iconUri == newItem.description.iconUri + oldItem.description.subtitle.toString() == newItem.description.subtitle.toString() } } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/QueueSheetFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/QueueSheetFragment.kt index 5ce9b1f0c..cba142470 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/QueueSheetFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/QueueSheetFragment.kt @@ -88,7 +88,9 @@ class QueueSheetFragment : Fragment() { } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - viewModel.mediaController?.removeQueueItem(adapter.getItem(viewHolder.absoluteAdapterPosition).description) + val index = viewHolder.absoluteAdapterPosition + adapter.removeItem(index) + MediaSessionConnection.binder?.songPlayer?.player?.removeMediaItem(index) } }) From ea7c9cc2c29b011933392834c8c44c6f634bf603 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Wed, 19 Oct 2022 23:12:13 +0800 Subject: [PATCH 09/95] Hide download action for YT items temporarily (#317) --- app/src/main/res/menu/youtube_item.xml | 1 + app/src/main/res/menu/youtube_item_batch.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/res/menu/youtube_item.xml b/app/src/main/res/menu/youtube_item.xml index 8b0fd6be7..1ab2a8f61 100644 --- a/app/src/main/res/menu/youtube_item.xml +++ b/app/src/main/res/menu/youtube_item.xml @@ -26,6 +26,7 @@ android:title="@string/menu_add_to_playlist" /> \ No newline at end of file From 883631c0b69c0c7ca31dc24231ac5d4186a90175 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Thu, 20 Oct 2022 13:06:21 +0800 Subject: [PATCH 10/95] Delete persistent queue when restoring database --- .../main/java/com/zionhuang/music/playback/SongPlayer.kt | 8 ++++---- .../fragments/settings/BackupRestoreSettingsFragment.kt | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt index 5561c95c4..6b1ed15e0 100644 --- a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt +++ b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt @@ -303,7 +303,7 @@ class SongPlayer( } if (context.sharedPreferences.getBoolean(context.getString(R.string.pref_persistent_queue), true)) { runCatching { - context.filesDir.resolve(PERSIST_QUEUE_FILE).inputStream().use { fis -> + context.filesDir.resolve(PERSISTENT_QUEUE_FILE).inputStream().use { fis -> ObjectInputStream(fis).use { oos -> oos.readObject() as PersistQueue } @@ -586,7 +586,7 @@ class SongPlayer( private fun saveQueueToDisk() { if (player.playbackState == STATE_IDLE) { - context.filesDir.resolve(PERSIST_QUEUE_FILE).delete() + context.filesDir.resolve(PERSISTENT_QUEUE_FILE).delete() return } val persistQueue = PersistQueue( @@ -596,7 +596,7 @@ class SongPlayer( position = player.currentPosition ) runCatching { - context.filesDir.resolve(PERSIST_QUEUE_FILE).outputStream().use { fos -> + context.filesDir.resolve(PERSISTENT_QUEUE_FILE).outputStream().use { fos -> ObjectOutputStream(fos).use { oos -> oos.writeObject(persistQueue) } @@ -630,7 +630,7 @@ class SongPlayer( const val NOTIFICATION_ID = 888 const val ERROR_CODE_NO_STREAM = 1000001 const val CHUNK_LENGTH = 512 * 1024L - const val PERSIST_QUEUE_FILE = "persist_queue.data" + const val PERSISTENT_QUEUE_FILE = "persistent_queue.data" fun createPendingIntent(context: Context, action: String, instanceId: Int): PendingIntent = PendingIntent.getBroadcast( context, diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/settings/BackupRestoreSettingsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/settings/BackupRestoreSettingsFragment.kt index 77b1ff8a0..0d9d9bde7 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/settings/BackupRestoreSettingsFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/settings/BackupRestoreSettingsFragment.kt @@ -7,7 +7,6 @@ import android.widget.Toast.LENGTH_SHORT import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.zionhuang.music.R import com.zionhuang.music.db.MusicDatabase @@ -15,6 +14,7 @@ import com.zionhuang.music.db.MusicDatabase.Companion.DB_NAME import com.zionhuang.music.db.checkpoint import com.zionhuang.music.extensions.zipInputStream import com.zionhuang.music.extensions.zipOutputStream +import com.zionhuang.music.playback.SongPlayer.Companion.PERSISTENT_QUEUE_FILE import com.zionhuang.music.ui.activities.MainActivity import com.zionhuang.music.ui.fragments.base.BaseSettingsFragment import java.io.File @@ -79,6 +79,7 @@ class BackupRestoreSettingsFragment : BaseSettingsFragment() { entry = inputStream.nextEntry } } + requireContext().filesDir.resolve(PERSISTENT_QUEUE_FILE).delete() requireContext().startActivity(Intent(requireContext(), MainActivity::class.java)) exitProcess(0) }.onFailure { From c34b03a141f7bc6dba75f6754984309e2dd8d3e1 Mon Sep 17 00:00:00 2001 From: gidano Date: Thu, 20 Oct 2022 19:21:12 +0200 Subject: [PATCH 11/95] Edited Hungarian translation --- values-hu/strings.xml | 274 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 values-hu/strings.xml diff --git a/values-hu/strings.xml b/values-hu/strings.xml new file mode 100644 index 000000000..c457d2e29 --- /dev/null +++ b/values-hu/strings.xml @@ -0,0 +1,274 @@ + + + Kezdőlap + Dalok + Előadók + Albumok + Listák + Fedezd fel + Beállítások + Most játszva + Hibajelentés + + + Megjelenés + Rendszertéma szerint + Téma szín + Sötét téma + Be + Ki + Alapért. nyitott lap + A navigációs lapok testreszabása + Rendszer szerint + + Tartalom + App nyelve + A tartalom alapért. nyelve + A tartalom alapért.t országa + Proxy bekapcsolása + Proxy típusa + Proxy URL + Indítsa újra, hogy életbe lépjen + + Lejátszó és hang + Állandó várósor + Hangminőség + Auto + Magas + Gyenge + Equalizer + + Gyorsítótár + Max. kép gyors.tár + Kép gyors.tár törlés + Max. dal gyors.tár + %s használva + + Általános + Auto. letöltés + Töltse le a dalt, amikor hozzáadja a könyvtárhoz + Dal auto. hozzáadása a könyvtárhoz + Adja hozzá a dalt a könyvtárához, amikor a lejátszás befejeződött + Bontsa ki az alsó lejátszót lejátszás közben + + Adatvédelem + Keresési előzmények szüneteltetése + Keresési előzmények törlése + Biztosan töröl minden keresési előzményt? + + Bizt.mentés és visszaállítás + Bizt.mentés + Visszaállítás + + Rólunk + App verzió + + + Szakura + Vörös + Pink + Bíbor + Mély bíbor + Indigókék + Kék + Világoskék + Cián + Zöldeskék + Zöld + Világoszöld + Lime + Sárga + Amber + Narancs + Mély narancs + Barna + Kékesszürke + + + Keresés + YouTube Music keresés… + Keresés könyvtárban… + + + Tetszett dalok + Letöltött dalok + + + Szerkeszt + Rádió indítás + Lejátszás + Következő + Várósorhoz ad + Könyvtárhoz ad + Letöltés + Letöltés eltávolítása + Lista importálása + Lejátszólistához ad + Előadó megtekintése + Album megtekintése + Újrahív + Megosztás + Törlés + + + Dal szerkesztése + Dal címe + Dal előadója + A cím nem lehet üres. + Az előadó nem lehet üres. + Ment + + Lista létrehozás + Lista neve + A lejátszólista neve nem lehet üres. + + Előadó szerkesztése + Előadó neve + Az előadó neve nem lehet üres. + + Duplikált előadók + %1$s előadó már létezik. + + Válassz lejátszási listát + + Lejátszólista szerkesztés + + Válasszon biztonsági másolatot + Válasszon visszaállítható tartalmat + Preferenciák + Adatbázis + Letöltött dalok + A bizt.mentés sikeresen létrehozva + Nem sikerült biztonsági mentést készíteni + Nem sikerült visszaállítani a biztonsági másolatot + + + Zenelejátszó + Letöltés + + + + %d dal + %d dal + + + %d előadó + %d előadó + + + %d album + %d album + + + %d lejátszólista + %d lejátszólista + + + + Újra + Lejátszás + Mind lejátszása + Rádió + Keverés + Veremkövetés másolása + Jelentés + Jelentés GitHub-on + + + Dátum hozzáadva + Név + Előadó + Év + Dal szám + Hossz + Lejátszási idő + + + + %d dal törölve. + %d dal törölve. + + + %d kijelölve + %d kijelölve + + Visszavon + Az URL nem azonosítható. + + Dal szól legközelebb + %d dal szól legközelebb + + + Előadó szól legközelebb + %d előadó szól legközelebb + + + Album szól legközelebb + %d album szól legközelebb + + + Lista szól legközelebb + %d lista szól legközelebb + + A kiválasztottak szólnak legközelebb + + Dal hozzáadva a sorhoz + %d dal hozzáadva a sorhoz + + + Előadó hozzáadva a sorhoz + %d előadó hozzáadva a sorhoz + + + Album hozzáadva a sorhoz + %d album hozzáadva a sorhoz + + + Lista hozzáadva a sorhoz + %d lista sorhoz adva + + A kiválasztott hozzáadva a sorhoz + Könyvtárhoz adva + Eltávolítva könyvtárból + Lista importálva + Hozzáadva ide, %1$s + + Dal letöltés indítása + Kezdje el %d dal letöltését + + Eltávolított letöltés + Nézet + + + Tetszik + Tetszik eltávolítása + Könyvtárhoz ad + Eltávolít a könyvtárból + + + Mind + Dalok + Videók + Albumok + Előadók + Listák + Közösségi listák + Kiemelt lejátszási listák + + Rendszer alapérték + Saját könyvtárból + + + Sajnálom, ennek nem lett volna szabad megtörténnie. + Másolva a vágólapra + + + Nincs elérhető adatfolyam + Nincs hálózati kapcsolat + Időn túl + Ismeretlen hiba + + + Minden dal + Keresett dalok + \ No newline at end of file From 613a2bd85697cb0a0c93e5afeac90e5b00757324 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Fri, 21 Oct 2022 17:59:12 +0800 Subject: [PATCH 12/95] Fix #342 --- .../java/com/zionhuang/innertube/YouTube.kt | 31 ++++--------------- .../innertube/models/PlaylistSongInfo.kt | 9 ------ .../models/response/BrowseResponse.kt | 2 +- .../com/zionhuang/innertube/YouTubeTest.kt | 5 ++- 4 files changed, 9 insertions(+), 38 deletions(-) delete mode 100644 innertube/src/main/java/com/zionhuang/innertube/models/PlaylistSongInfo.kt diff --git a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt index adf91433a..ee4f0d6f3 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt @@ -44,9 +44,9 @@ object YouTube { suspend fun searchAllType(query: String): Result = runCatching { val response = innerTube.search(WEB_REMIX, query).body() BrowseResult( - items = response.contents!!.tabbedSearchResultsRenderer.tabs[0].tabRenderer.content!!.sectionListRenderer!!.contents - .flatMap { it.toBaseItems() } - .map { + items = response.contents?.tabbedSearchResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents + ?.flatMap { it.toBaseItems() } + ?.map { when (it) { is Header -> it.copy(moreNavigationEndpoint = null) // remove search type arrow link is SongItem -> it.copy(subtitle = it.subtitle.substringAfter(" • ")) @@ -55,7 +55,7 @@ object YouTube { is ArtistItem -> it.copy(subtitle = it.subtitle.substringAfter(" • ")) else -> it } - }, + }.orEmpty(), continuations = null ) } @@ -63,8 +63,8 @@ object YouTube { suspend fun search(query: String, filter: SearchFilter): Result = runCatching { val response = innerTube.search(WEB_REMIX, query, filter.value).body() BrowseResult( - items = response.contents!!.tabbedSearchResultsRenderer.tabs[0].tabRenderer.content!!.sectionListRenderer!!.contents[0].musicShelfRenderer!!.contents!!.mapNotNull { it.toItem() }, - continuations = response.contents.tabbedSearchResultsRenderer.tabs[0].tabRenderer.content!!.sectionListRenderer!!.contents[0].musicShelfRenderer!!.continuations?.getContinuations() + items = response.contents?.tabbedSearchResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicShelfRenderer?.contents?.mapNotNull { it.toItem() }.orEmpty(), + continuations = response.contents?.tabbedSearchResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicShelfRenderer?.continuations?.getContinuations() ) } @@ -115,25 +115,6 @@ object YouTube { ) } - - /** - * Calling "next" endpoint without continuation - * @return lyricsEndpoint, relatedEndpoint - */ - suspend fun getPlaylistSongInfo( - videoId: String, - playlistId: String? = null, - playlistSetVideoId: String? = null, - index: Int? = null, - params: String? = null, - ): Result = runCatching { - val response = innerTube.next(WEB_REMIX, videoId, playlistId, playlistSetVideoId, index, params).body() - PlaylistSongInfo( - lyricsEndpoint = response.contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer.watchNextTabbedResultsRenderer.tabs[1].tabRenderer.endpoint!!.browseEndpoint!!, - relatedEndpoint = response.contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer.watchNextTabbedResultsRenderer.tabs[2].tabRenderer.endpoint!!.browseEndpoint!!, - ) - } - suspend fun next(endpoint: WatchEndpoint, continuation: String? = null): Result = runCatching { val response = innerTube.next(WEB_REMIX, endpoint.videoId, endpoint.playlistId, endpoint.playlistSetVideoId, endpoint.index, endpoint.params, continuation).body() val playlistPanelRenderer = response.continuationContents?.playlistPanelContinuation diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/PlaylistSongInfo.kt b/innertube/src/main/java/com/zionhuang/innertube/models/PlaylistSongInfo.kt deleted file mode 100644 index eff427f6d..000000000 --- a/innertube/src/main/java/com/zionhuang/innertube/models/PlaylistSongInfo.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.zionhuang.innertube.models - -import kotlinx.serialization.Serializable - -@Serializable -data class PlaylistSongInfo( - val lyricsEndpoint: BrowseEndpoint, - val relatedEndpoint: BrowseEndpoint, -) \ No newline at end of file diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/response/BrowseResponse.kt b/innertube/src/main/java/com/zionhuang/innertube/models/response/BrowseResponse.kt index 88f4e8d5c..53d61899f 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/models/response/BrowseResponse.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/models/response/BrowseResponse.kt @@ -26,7 +26,7 @@ data class BrowseResponse( else -> throw UnsupportedOperationException("Unknown continuation type") } contents != null -> BrowseResult( - items = contents.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.flatMap { it.toBaseItems() } ?: emptyList(), + items = contents.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.flatMap { it.toBaseItems() }.orEmpty(), lyrics = contents.sectionListRenderer?.contents?.firstOrNull()?.musicDescriptionShelfRenderer?.description?.runs?.firstOrNull()?.text, urlCanonical = microformat?.microformatDataRenderer?.urlCanonical, continuations = contents.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicPlaylistShelfRenderer?.continuations?.getContinuations().orEmpty() diff --git a/innertube/src/test/java/com/zionhuang/innertube/YouTubeTest.kt b/innertube/src/test/java/com/zionhuang/innertube/YouTubeTest.kt index 0b4164354..0b21fa783 100644 --- a/innertube/src/test/java/com/zionhuang/innertube/YouTubeTest.kt +++ b/innertube/src/test/java/com/zionhuang/innertube/YouTubeTest.kt @@ -18,7 +18,8 @@ import io.ktor.client.engine.okhttp.* import io.ktor.client.request.* import io.ktor.http.* import kotlinx.coroutines.runBlocking -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Ignore import org.junit.Test @@ -120,8 +121,6 @@ class YouTubeTest { assertTrue(nextResult.items.isNotEmpty()) nextResult = youTube.next(WatchEndpoint(videoId = "jF4KKOsoyDs", playlistId = "PLaHh1PiehjvqOXm1J7b2QGy2iAvN84Azb")).getOrThrow() assertTrue(nextResult.items.isNotEmpty()) - val playlistSongInfo = youTube.getPlaylistSongInfo(videoId = VIDEO_IDS.random()).getOrThrow() - assertNotNull(playlistSongInfo.lyricsEndpoint) } @Test From dd81989e2c859da9d66cd03d72e71391e6c9b7ff Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Fri, 21 Oct 2022 22:23:07 +0800 Subject: [PATCH 13/95] Move value-hu to correct directory --- .../src/main/res/values-hu}/strings.xml | 545 +++++++++--------- 1 file changed, 272 insertions(+), 273 deletions(-) rename {values-hu => app/src/main/res/values-hu}/strings.xml (97%) diff --git a/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml similarity index 97% rename from values-hu/strings.xml rename to app/src/main/res/values-hu/strings.xml index c457d2e29..c42d173b7 100644 --- a/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -1,274 +1,273 @@ - - - Kezdőlap - Dalok - Előadók - Albumok - Listák - Fedezd fel - Beállítások - Most játszva - Hibajelentés - - - Megjelenés - Rendszertéma szerint - Téma szín - Sötét téma - Be - Ki - Alapért. nyitott lap - A navigációs lapok testreszabása - Rendszer szerint - - Tartalom - App nyelve - A tartalom alapért. nyelve - A tartalom alapért.t országa - Proxy bekapcsolása - Proxy típusa - Proxy URL - Indítsa újra, hogy életbe lépjen - - Lejátszó és hang - Állandó várósor - Hangminőség - Auto - Magas - Gyenge - Equalizer - - Gyorsítótár - Max. kép gyors.tár - Kép gyors.tár törlés - Max. dal gyors.tár - %s használva - - Általános - Auto. letöltés - Töltse le a dalt, amikor hozzáadja a könyvtárhoz - Dal auto. hozzáadása a könyvtárhoz - Adja hozzá a dalt a könyvtárához, amikor a lejátszás befejeződött - Bontsa ki az alsó lejátszót lejátszás közben - - Adatvédelem - Keresési előzmények szüneteltetése - Keresési előzmények törlése - Biztosan töröl minden keresési előzményt? - - Bizt.mentés és visszaállítás - Bizt.mentés - Visszaállítás - - Rólunk - App verzió - - - Szakura - Vörös - Pink - Bíbor - Mély bíbor - Indigókék - Kék - Világoskék - Cián - Zöldeskék - Zöld - Világoszöld - Lime - Sárga - Amber - Narancs - Mély narancs - Barna - Kékesszürke - - - Keresés - YouTube Music keresés… - Keresés könyvtárban… - - - Tetszett dalok - Letöltött dalok - - - Szerkeszt - Rádió indítás - Lejátszás - Következő - Várósorhoz ad - Könyvtárhoz ad - Letöltés - Letöltés eltávolítása - Lista importálása - Lejátszólistához ad - Előadó megtekintése - Album megtekintése - Újrahív - Megosztás - Törlés - - - Dal szerkesztése - Dal címe - Dal előadója - A cím nem lehet üres. - Az előadó nem lehet üres. - Ment - - Lista létrehozás - Lista neve - A lejátszólista neve nem lehet üres. - - Előadó szerkesztése - Előadó neve - Az előadó neve nem lehet üres. - - Duplikált előadók - %1$s előadó már létezik. - - Válassz lejátszási listát - - Lejátszólista szerkesztés - - Válasszon biztonsági másolatot - Válasszon visszaállítható tartalmat - Preferenciák - Adatbázis - Letöltött dalok - A bizt.mentés sikeresen létrehozva - Nem sikerült biztonsági mentést készíteni - Nem sikerült visszaállítani a biztonsági másolatot - - - Zenelejátszó - Letöltés - - - - %d dal - %d dal - - - %d előadó - %d előadó - - - %d album - %d album - - - %d lejátszólista - %d lejátszólista - - - - Újra - Lejátszás - Mind lejátszása - Rádió - Keverés - Veremkövetés másolása - Jelentés - Jelentés GitHub-on - - - Dátum hozzáadva - Név - Előadó - Év - Dal szám - Hossz - Lejátszási idő - - - - %d dal törölve. - %d dal törölve. - - - %d kijelölve - %d kijelölve - - Visszavon - Az URL nem azonosítható. - - Dal szól legközelebb - %d dal szól legközelebb - - - Előadó szól legközelebb - %d előadó szól legközelebb - - - Album szól legközelebb - %d album szól legközelebb - - - Lista szól legközelebb - %d lista szól legközelebb - - A kiválasztottak szólnak legközelebb - - Dal hozzáadva a sorhoz - %d dal hozzáadva a sorhoz - - - Előadó hozzáadva a sorhoz - %d előadó hozzáadva a sorhoz - - - Album hozzáadva a sorhoz - %d album hozzáadva a sorhoz - - - Lista hozzáadva a sorhoz - %d lista sorhoz adva - - A kiválasztott hozzáadva a sorhoz - Könyvtárhoz adva - Eltávolítva könyvtárból - Lista importálva - Hozzáadva ide, %1$s - - Dal letöltés indítása - Kezdje el %d dal letöltését - - Eltávolított letöltés - Nézet - - - Tetszik - Tetszik eltávolítása - Könyvtárhoz ad - Eltávolít a könyvtárból - - - Mind - Dalok - Videók - Albumok - Előadók - Listák - Közösségi listák - Kiemelt lejátszási listák - - Rendszer alapérték - Saját könyvtárból - - - Sajnálom, ennek nem lett volna szabad megtörténnie. - Másolva a vágólapra - - - Nincs elérhető adatfolyam - Nincs hálózati kapcsolat - Időn túl - Ismeretlen hiba - - - Minden dal - Keresett dalok + + + Kezdőlap + Dalok + Előadók + Albumok + Listák + Fedezd fel + Beállítások + Most játszva + Hibajelentés + + + Megjelenés + Rendszertéma szerint + Téma szín + Sötét téma + Be + Ki + Alapért. nyitott lap + A navigációs lapok testreszabása + Rendszer szerint + + Tartalom + A tartalom alapért. nyelve + A tartalom alapért.t országa + Proxy bekapcsolása + Proxy típusa + Proxy URL + Indítsa újra, hogy életbe lépjen + + Lejátszó és hang + Állandó várósor + Hangminőség + Auto + Magas + Gyenge + Equalizer + + Gyorsítótár + Max. kép gyors.tár + Kép gyors.tár törlés + Max. dal gyors.tár + %s használva + + Általános + Auto. letöltés + Töltse le a dalt, amikor hozzáadja a könyvtárhoz + Dal auto. hozzáadása a könyvtárhoz + Adja hozzá a dalt a könyvtárához, amikor a lejátszás befejeződött + Bontsa ki az alsó lejátszót lejátszás közben + + Adatvédelem + Keresési előzmények szüneteltetése + Keresési előzmények törlése + Biztosan töröl minden keresési előzményt? + + Bizt.mentés és visszaállítás + Bizt.mentés + Visszaállítás + + Rólunk + App verzió + + + Szakura + Vörös + Pink + Bíbor + Mély bíbor + Indigókék + Kék + Világoskék + Cián + Zöldeskék + Zöld + Világoszöld + Lime + Sárga + Amber + Narancs + Mély narancs + Barna + Kékesszürke + + + Keresés + YouTube Music keresés… + Keresés könyvtárban… + + + Tetszett dalok + Letöltött dalok + + + Szerkeszt + Rádió indítás + Lejátszás + Következő + Várósorhoz ad + Könyvtárhoz ad + Letöltés + Letöltés eltávolítása + Lista importálása + Lejátszólistához ad + Előadó megtekintése + Album megtekintése + Újrahív + Megosztás + Törlés + + + Dal szerkesztése + Dal címe + Song artists + A cím nem lehet üres. + Az előadó nem lehet üres. + Ment + + Lista létrehozás + Lista neve + A lejátszólista neve nem lehet üres. + + Előadó szerkesztése + Előadó neve + Az előadó neve nem lehet üres. + + Duplikált előadók + %1$s előadó már létezik. + + Válassz lejátszási listát + + Lejátszólista szerkesztés + + Válasszon biztonsági másolatot + Válasszon visszaállítható tartalmat + Preferenciák + Adatbázis + Letöltött dalok + A bizt.mentés sikeresen létrehozva + Nem sikerült biztonsági mentést készíteni + Nem sikerült visszaállítani a biztonsági másolatot + + + Zenelejátszó + Letöltés + + + + %d dal + %d dal + + + %d előadó + %d előadó + + + %d album + %d album + + + %d lejátszólista + %d lejátszólista + + + + Újra + Lejátszás + Mind lejátszása + Rádió + Keverés + Veremkövetés másolása + Jelentés + Jelentés GitHub-on + + + Dátum hozzáadva + Név + Előadó + Év + Dal szám + Hossz + Lejátszási idő + + + + %d dal törölve. + %d dal törölve. + + + %d kijelölve + %d kijelölve + + Visszavon + Az URL nem azonosítható. + + Dal szól legközelebb + %d dal szól legközelebb + + + Előadó szól legközelebb + %d előadó szól legközelebb + + + Album szól legközelebb + %d album szól legközelebb + + + Lista szól legközelebb + %d lista szól legközelebb + + A kiválasztottak szólnak legközelebb + + Dal hozzáadva a sorhoz + %d dal hozzáadva a sorhoz + + + Előadó hozzáadva a sorhoz + %d előadó hozzáadva a sorhoz + + + Album hozzáadva a sorhoz + %d album hozzáadva a sorhoz + + + Lista hozzáadva a sorhoz + %d lista sorhoz adva + + A kiválasztott hozzáadva a sorhoz + Könyvtárhoz adva + Eltávolítva könyvtárból + Lista importálva + Hozzáadva ide, %1$s + + Dal letöltés indítása + Kezdje el %d dal letöltését + + Eltávolított letöltés + Nézet + + + Tetszik + Tetszik eltávolítása + Könyvtárhoz ad + Eltávolít a könyvtárból + + + Mind + Dalok + Videók + Albumok + Előadók + Listák + Közösségi listák + Kiemelt lejátszási listák + + Rendszer alapérték + Saját könyvtárból + + + Sajnálom, ennek nem lett volna szabad megtörténnie. + Másolva a vágólapra + + + Nincs elérhető adatfolyam + Nincs hálózati kapcsolat + Időn túl + Ismeretlen hiba + + + Minden dal + Keresett dalok \ No newline at end of file From 354fb30d00dcd65be2f61f9824192e4e40695e82 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Fri, 21 Oct 2022 22:34:22 +0800 Subject: [PATCH 14/95] Make youtube item title bold --- app/src/main/res/layout/item_youtube_list.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/item_youtube_list.xml b/app/src/main/res/layout/item_youtube_list.xml index 06ddb530f..f37eaee57 100644 --- a/app/src/main/res/layout/item_youtube_list.xml +++ b/app/src/main/res/layout/item_youtube_list.xml @@ -92,6 +92,7 @@ android:text="@{item.title}" android:textColor="?android:textColorPrimary" android:textSize="14sp" + android:textStyle="bold" app:layout_constraintBottom_toTopOf="@+id/item_details" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" From a97f05c485baf8748bc9fcf05961f5724672fe06 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Fri, 21 Oct 2022 22:48:23 +0800 Subject: [PATCH 15/95] Add missing translation fields in Hungarian --- app/src/main/res/values-hu/strings.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index c42d173b7..10d650780 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -93,6 +93,7 @@ Letöltött dalok + Details Szerkeszt Rádió indítás Lejátszás @@ -110,6 +111,16 @@ Törlés + Details + Media id + MIME type + Codecs + Bitrate + Sample rate + Loudness + File size + Unknown + Dal szerkesztése Dal címe Song artists From 9f92512a59bfdbb61347b3ff6f527ed612cdaafa Mon Sep 17 00:00:00 2001 From: gidano Date: Fri, 21 Oct 2022 17:05:20 +0200 Subject: [PATCH 16/95] Update strings.xml --- app/src/main/res/values-hu/strings.xml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 10d650780..e04f10c32 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -93,7 +93,7 @@ Letöltött dalok - Details + Részletek Szerkeszt Rádió indítás Lejátszás @@ -111,19 +111,19 @@ Törlés - Details - Media id - MIME type - Codecs - Bitrate - Sample rate - Loudness - File size - Unknown + Részletek + Média id + MIME típus + Kodekek + Bitráta + Mintavétel + Hangerősség + Fájl méret + Ismeretlen Dal szerkesztése Dal címe - Song artists + Dal előadói A cím nem lehet üres. Az előadó nem lehet üres. Ment @@ -281,4 +281,4 @@ Minden dal Keresett dalok - \ No newline at end of file + From b49ce220765f122547959b7eec36db6e0678b04d Mon Sep 17 00:00:00 2001 From: fabianski7 <47755037+fabianski7@users.noreply.github.com> Date: Sat, 22 Oct 2022 13:43:36 +0000 Subject: [PATCH 17/95] Brazilian Portuguese translation update --- app/src/main/res/values-pt-rBR/strings.xml | 224 ++++++++++----------- 1 file changed, 112 insertions(+), 112 deletions(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 30cb23e70..4bc4dcf23 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1,63 +1,63 @@ - Home + Início Músicas Artistas - Albums + Álbuns Playlists Explorar Configurações Tocando agora - Error report + Reportar erro - Appearance - Follow system theme - Theme color - Dark theme + Aparência + Seguir o tema do sistema + Cor do tema + Tema escuro On Off - Default open tab - Customize navigation tabs - Follow system + Aba padrão ao iniciar + Costumar barra de navegação + Seguir o sistema - Content + Conteúdo Idioma padrão do conteúdo País padrão do conteúdo - Enable proxy - Proxy type - Proxy URL - Restart to take effect + Ativar proxy + Tipo de proxy + URL do proxy + Reiniciar para ativar proxy - Player and audio - Persistent queue - Audio quality - Auto - High - Low - Equalizer + Player e áudio + Fila persistente + Qualidade do áudio + Automática + Alta + Baixa + Equalizador Cache - Max image cache size - Clear image cache - Max song cache size - %s used + Tamanho máximo do chace de imagens + Limpar cache de imagens + Tamanho máximo do cache de músicas + %s usados - General + Geral Fazer download automaticamente Fazer download de música quando adicionada à biblioteca Adicionar música à biblioteca automaticamente Adicionar música à sua biblioteca quando terminar de ser reproduzida Expandir player ao reproduzir - Privacy - Pause search history - Clear search history - Are you sure to clear all search history? + Privacidade + Pausar histórico de pesquisa + Limpar histórico de pesquisa + Tem certeza que deseja deletar todo o seu histórico de pesquisa? - Backup and restore + Backup e restauração Backup - Restore + Restaurar Sobre Versão do aplicativo @@ -85,41 +85,41 @@ Pesquisar - Search YouTube Music… - Search library… + Pesquisar no YouTube Music… + Pesquisar na biblioteca… - Liked songs - Downloaded songs + Músicas favoritas + Músicas baixadas - Details + Detalhes Editar - Start radio - Play + Iniciar rádio + Tocar Tocar em seguida Adicionar à fila Adicionar à biblioteca Download Remover download - Import playlist + Importar playlist Adicionar à playlist - View artist - View album - Refetch - Share + Ver artista + Ver álbum + Atualizar + Compartilhar Excluir - Details + Detalhes Media id MIME type Codecs Bitrate Sample rate Loudness - File size - Unknown + Tamanho do arquivo + Desconhecido Editar música Título da Música @@ -136,21 +136,21 @@ Nome do artista O nome do artista não pode ficar vazio. - Duplicate artists - Artist %1$s already exists. + Artistas duplicados + O artista %1$s já existe. Escolher playlist Editar playlist - Choose backup content - Choose restore content - Preferences - Database - Downloaded songs - Backup created successfully - Couldn\'t create backup - Failed to restore backup + Escolha o conteúdo do backup + Escolha o conteúdo a ser restaurado + Preferências + Banco de dados + Músicas baixadas + Backup criado com sucesso + Não foi possível criar o backup + Falha ao restaurar o backup Reprodutor de Música @@ -162,12 +162,12 @@ %d músicas - %d artist - %d artists + %d artista + %d artistas - %d album - %d albums + %d álbum + %d álbuns %d playlist @@ -176,22 +176,22 @@ Tentar novamente - Play + Tocar Tocar tudo - Radio + Rádio Aleatório - Copy stacktrace - Report - Report on GitHub + Copiar stacktrace + Reportar + Reportar no GitHub Quando adicionada Nome Artista - Year - Song count - Length - Play time + Ano + Número da faixa + Tamanho + Tempo de reprodução @@ -203,57 +203,57 @@ %d selecionadas Desfazer - Can\'t identify this url. + Não foi possível identificar essa url. - %d song will play next - %d songs will play next + %d música será reproduzida em seguida + %d músicas serão reproduzidas em seguida - %d artist will play next - %d artists will play next + %d artista será reproduzido em seguida + %d artistas serão reproduzidos em seguida - %d album will play next - %d albums will play next + %d álbum será reproduzido em seguida + %d álguns serão reproduzidos em seguida - %d playlist will play next - %d playlists will play next + %d playlist será reproduzida em seguida + %d playlists serão reproduzidas em seguida - Selected will play next + Selecionados irão ser reproduzidos em seguida - %d song added to queue - %d songs added to queue + %d música adicionada à fila + %d músicas adicionadas à fila - %d artist added to queue - %d artists added to queue + %d artista adicionado à fila + %d artistas adicionados à fila - %d album added to queue - %d albums added to queue + %d álbum adicionado à fila + %d álbuns adicionados à fila - %d playlist added to queue - %d playlists added to queue + %d playlist adicionada à fila + %d playlists adicionadas à fila - Selected added to queue - Added to library - Removed from library - Playlist imported - Added to %1$s + Selecionadas adicionadas à fila + Adicionado à biblioteca + Removido da biblioteca + Playlist importada + Adicionado em %1$s - Start downloading %d song - Start downloading %d songs + Iniciando o download de %d músicas + Iniciando o download de %d música - Removed download - View + Download removido + Ver - Like - Remove like + Favoritar + Remover dos favoritos Adicionar à biblioteca - Remove from library + Remover da biblioteca Tudo @@ -262,23 +262,23 @@ Álbuns Artistas Playlists - Community playlists - Featured playlists + Playlists da comunidade + Playlists em destaque Padrão do sistema - From your library + Da sua biblioteca - Sorry, that should not have happened. - Copied to clipboard + Desculpa, isso não deveria ter acontecido. + Copiado para à área de transferência - No stream available - No network connection - Timeout - Unknown error + Nenhum canal de reprodução disponível + Sem conexão com à internet + Tempo esgotado + Erro desconhecido - All songs - Searched songs + Todas as músicas + Músicas pesquisadas From f6f0b74832a13ecd8780bdb856515c35e60d148e Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sat, 22 Oct 2022 23:46:19 +0800 Subject: [PATCH 18/95] [Feature] Lyrics (#76) Note: experimental feature --- app/build.gradle.kts | 2 + .../4.json | 744 ++++++++++++++++++ app/src/main/java/com/zionhuang/music/App.kt | 4 + .../com/zionhuang/music/db/MusicDatabase.kt | 9 +- .../com/zionhuang/music/db/daos/LyricsDao.kt | 29 + .../com/zionhuang/music/db/daos/SongDao.kt | 4 +- .../music/db/entities/LyricsEntity.kt | 14 + .../music/lyrics/KuGouLyricsProvider.kt | 8 + .../zionhuang/music/lyrics/LyricsProvider.kt | 5 + .../music/lyrics/YouTubeLyricsProvider.kt | 14 + .../zionhuang/music/playback/SongPlayer.kt | 37 +- .../zionhuang/music/repos/SongRepository.kt | 19 +- .../music/repos/base/LocalRepository.kt | 13 + .../ui/fragments/BottomControlsFragment.kt | 30 +- .../music/ui/fragments/QueueSheetFragment.kt | 7 + .../zionhuang/music/ui/widgets/LyricsView.kt | 491 ++++++++++++ .../music/utils/lyrics/LyricsEntry.kt | 37 + .../music/utils/lyrics/LyricsUtils.kt | 46 ++ .../music/viewmodels/PlaybackViewModel.kt | 11 +- app/src/main/res/drawable/ic_lyrics.xml | 13 + .../res/layout-land/bottom_controls_sheet.xml | 21 +- .../main/res/layout/bottom_controls_sheet.xml | 22 +- app/src/main/res/layout/queue_sheet.xml | 12 + app/src/main/res/values-es-rUS/strings.xml | 3 + app/src/main/res/values-es/strings.xml | 3 + app/src/main/res/values-fa-rIR/strings.xml | 3 + app/src/main/res/values-fi-rFI/strings.xml | 3 + app/src/main/res/values-fr-rFR/strings.xml | 3 + app/src/main/res/values-hu/strings.xml | 3 + app/src/main/res/values-it/strings.xml | 3 + app/src/main/res/values-ja-rJP/strings.xml | 3 + app/src/main/res/values-ko-rKR/strings.xml | 3 + app/src/main/res/values-ml-rIN/strings.xml | 3 + app/src/main/res/values-pt-rBR/strings.xml | 3 + app/src/main/res/values-sv-rSE/strings.xml | 3 + app/src/main/res/values-zh-rCN/strings.xml | 3 + app/src/main/res/values-zh-rTW/strings.xml | 3 + app/src/main/res/values/lyrics_colors.xml | 9 + app/src/main/res/values/lyrics_dimens.xml | 10 + app/src/main/res/values/lyrics_view_attrs.xml | 25 + app/src/main/res/values/strings.xml | 3 + innertube/build.gradle.kts | 6 +- .../java/com/zionhuang/innertube/YouTube.kt | 1 + kugou/.gitignore | 1 + kugou/build.gradle.kts | 27 + .../main/java/com/zionhuang/kugou/KuGou.kt | 157 ++++ .../kugou/models/DownloadLyricsResponse.kt | 8 + .../kugou/models/SearchLyricsResponse.kt | 23 + .../kugou/models/SearchSongResponse.kt | 22 + kugou/src/test/java/Test.kt | 15 + settings.gradle.kts | 1 + 51 files changed, 1918 insertions(+), 24 deletions(-) create mode 100644 app/schemas/com.zionhuang.music.db.MusicDatabase/4.json create mode 100644 app/src/main/java/com/zionhuang/music/db/daos/LyricsDao.kt create mode 100644 app/src/main/java/com/zionhuang/music/db/entities/LyricsEntity.kt create mode 100644 app/src/main/java/com/zionhuang/music/lyrics/KuGouLyricsProvider.kt create mode 100644 app/src/main/java/com/zionhuang/music/lyrics/LyricsProvider.kt create mode 100644 app/src/main/java/com/zionhuang/music/lyrics/YouTubeLyricsProvider.kt create mode 100644 app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt create mode 100644 app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsEntry.kt create mode 100644 app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsUtils.kt create mode 100644 app/src/main/res/drawable/ic_lyrics.xml create mode 100644 app/src/main/res/values/lyrics_colors.xml create mode 100644 app/src/main/res/values/lyrics_dimens.xml create mode 100644 app/src/main/res/values/lyrics_view_attrs.xml create mode 100644 kugou/.gitignore create mode 100644 kugou/build.gradle.kts create mode 100644 kugou/src/main/java/com/zionhuang/kugou/KuGou.kt create mode 100644 kugou/src/main/java/com/zionhuang/kugou/models/DownloadLyricsResponse.kt create mode 100644 kugou/src/main/java/com/zionhuang/kugou/models/SearchLyricsResponse.kt create mode 100644 kugou/src/main/java/com/zionhuang/kugou/models/SearchSongResponse.kt create mode 100644 kugou/src/test/java/Test.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 907671420..744700201 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -163,6 +163,8 @@ dependencies { testImplementation("androidx.room:room-testing:2.4.3") // YouTube API implementation(project(mapOf("path" to ":innertube"))) + // KuGou + implementation(project(mapOf("path" to ":kugou"))) // Apache Utils implementation("org.apache.commons:commons-lang3:3.12.0") implementation("org.apache.commons:commons-text:1.9") diff --git a/app/schemas/com.zionhuang.music.db.MusicDatabase/4.json b/app/schemas/com.zionhuang.music.db.MusicDatabase/4.json new file mode 100644 index 000000000..dac17cfbc --- /dev/null +++ b/app/schemas/com.zionhuang.music.db.MusicDatabase/4.json @@ -0,0 +1,744 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "fe70b678dc51b8cad5fd1cb4eadbb95d", + "entities": [ + { + "tableName": "song", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `liked` INTEGER NOT NULL, `totalPlayTime` INTEGER NOT NULL, `isTrash` INTEGER NOT NULL, `download_state` INTEGER NOT NULL, `create_date` INTEGER NOT NULL, `modify_date` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumName", + "columnName": "albumName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "liked", + "columnName": "liked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalPlayTime", + "columnName": "totalPlayTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTrash", + "columnName": "isTrash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createDate", + "columnName": "create_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "modifyDate", + "columnName": "modify_date", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "artist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `bannerUrl` TEXT, `description` TEXT, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createDate", + "columnName": "createDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "album", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "songCount", + "columnName": "songCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createDate", + "columnName": "createDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "playlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT, `authorId` TEXT, `year` INTEGER, `thumbnailUrl` TEXT, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createDate", + "columnName": "createDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "song_artist_map", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "songId", + "artistId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_song_artist_map_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_song_artist_map_artistId", + "unique": false, + "columnNames": [ + "artistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)" + } + ], + "foreignKeys": [ + { + "table": "song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "artist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "artistId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "song_album_map", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "songId", + "albumId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_song_album_map_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_song_album_map_albumId", + "unique": false, + "columnNames": [ + "albumId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)" + } + ], + "foreignKeys": [ + { + "table": "song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "album", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "albumId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "album_artist_map", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "albumId", + "artistId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_album_artist_map_albumId", + "unique": false, + "columnNames": [ + "albumId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)" + }, + { + "name": "index_album_artist_map_artistId", + "unique": false, + "columnNames": [ + "artistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)" + } + ], + "foreignKeys": [ + { + "table": "album", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "albumId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "artist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "artistId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "playlist_song_map", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlistId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_playlist_song_map_playlistId", + "unique": false, + "columnNames": [ + "playlistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)" + }, + { + "name": "index_playlist_song_map_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)" + } + ], + "foreignKeys": [ + { + "table": "playlist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "playlistId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `songId` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "search_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "query", + "columnName": "query", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_search_history_query", + "unique": true, + "columnNames": [ + "query" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "format", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itag", + "columnName": "itag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "codecs", + "columnName": "codecs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sampleRate", + "columnName": "sampleRate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "contentLength", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loudnessDb", + "columnName": "loudnessDb", + "affinity": "REAL", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "lyrics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lyrics", + "columnName": "lyrics", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [ + { + "viewName": "sorted_song_artist_map", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position" + }, + { + "viewName": "playlist_song_map_preview", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position" + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fe70b678dc51b8cad5fd1cb4eadbb95d')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/App.kt b/app/src/main/java/com/zionhuang/music/App.kt index f59c17ecf..bfdeac46d 100644 --- a/app/src/main/java/com/zionhuang/music/App.kt +++ b/app/src/main/java/com/zionhuang/music/App.kt @@ -11,6 +11,7 @@ import coil.disk.DiskCache import com.zionhuang.innertube.YouTube import com.zionhuang.innertube.models.YouTubeClient import com.zionhuang.innertube.models.YouTubeLocale +import com.zionhuang.kugou.KuGou import com.zionhuang.music.extensions.getEnum import com.zionhuang.music.extensions.sharedPreferences import com.zionhuang.music.extensions.toInetSocketAddress @@ -37,6 +38,9 @@ class App : Application(), ImageLoaderFactory { ?: languageTag.takeIf { it in languageCodes } ?: "en" ) + if (languageTag == "zh-TW") { + KuGou.useTraditionalChinese = true + } Log.d("App", "${YouTube.locale}") if (sharedPreferences.getBoolean(getString(R.string.pref_proxy_enabled), false)) { diff --git a/app/src/main/java/com/zionhuang/music/db/MusicDatabase.kt b/app/src/main/java/com/zionhuang/music/db/MusicDatabase.kt index 384492052..8049b9e21 100644 --- a/app/src/main/java/com/zionhuang/music/db/MusicDatabase.kt +++ b/app/src/main/java/com/zionhuang/music/db/MusicDatabase.kt @@ -28,16 +28,18 @@ import java.util.* PlaylistSongMap::class, DownloadEntity::class, SearchHistory::class, - FormatEntity::class + FormatEntity::class, + LyricsEntity::class ], views = [ SortedSongArtistMap::class, PlaylistSongMapPreview::class ], - version = 3, + version = 4, exportSchema = true, autoMigrations = [ - AutoMigration(from = 2, to = 3) + AutoMigration(from = 2, to = 3), + AutoMigration(from = 3, to = 4) ] ) @TypeConverters(Converters::class) @@ -49,6 +51,7 @@ abstract class MusicDatabase : RoomDatabase() { abstract val downloadDao: DownloadDao abstract val searchHistoryDao: SearchHistoryDao abstract val formatDao: FormatDao + abstract val lyricsDao: LyricsDao companion object { const val DB_NAME = "song.db" diff --git a/app/src/main/java/com/zionhuang/music/db/daos/LyricsDao.kt b/app/src/main/java/com/zionhuang/music/db/daos/LyricsDao.kt new file mode 100644 index 000000000..f8702ef6d --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/db/daos/LyricsDao.kt @@ -0,0 +1,29 @@ +package com.zionhuang.music.db.daos + +import androidx.room.* +import com.zionhuang.music.db.entities.LyricsEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface LyricsDao { + @Query("SELECT * FROM lyrics WHERE id = :id") + suspend fun getLyrics(id: String?): LyricsEntity? + + @Query("SELECT * FROM lyrics WHERE id = :id") + fun getLyricsAsFlow(id: String?): Flow + + @Query("SELECT EXISTS (SELECT 1 FROM lyrics WHERE id = :id)") + suspend fun hasLyrics(id: String): Boolean + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(lyrics: LyricsEntity): Long + + @Update + suspend fun update(lyrics: LyricsEntity) + + suspend fun upsert(lyrics: LyricsEntity) { + if (insert(lyrics) == -1L) { + update(lyrics) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/db/daos/SongDao.kt b/app/src/main/java/com/zionhuang/music/db/daos/SongDao.kt index c2124465f..5ed9afa66 100644 --- a/app/src/main/java/com/zionhuang/music/db/daos/SongDao.kt +++ b/app/src/main/java/com/zionhuang/music/db/daos/SongDao.kt @@ -84,10 +84,10 @@ interface SongDao { @Query("SELECT * FROM song WHERE title LIKE '%' || :query || '%' AND NOT isTrash LIMIT :previewSize") fun searchSongsPreview(query: String, previewSize: Int): Flow> - @Query("SELECT EXISTS (SELECT 1 FROM song WHERE id=:songId)") + @Query("SELECT EXISTS (SELECT 1 FROM song WHERE id = :songId)") suspend fun hasSong(songId: String): Boolean - @Query("SELECT EXISTS (SELECT 1 FROM song WHERE id=:songId)") + @Query("SELECT EXISTS (SELECT 1 FROM song WHERE id = :songId)") fun hasSongAsLiveData(songId: String): LiveData @Query("UPDATE song SET totalPlayTime = totalPlayTime + :playTime WHERE id = :songId") diff --git a/app/src/main/java/com/zionhuang/music/db/entities/LyricsEntity.kt b/app/src/main/java/com/zionhuang/music/db/entities/LyricsEntity.kt new file mode 100644 index 000000000..625e5e373 --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/db/entities/LyricsEntity.kt @@ -0,0 +1,14 @@ +package com.zionhuang.music.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "lyrics") +data class LyricsEntity( + @PrimaryKey val id: String, + val lyrics: String, +) { + companion object { + const val LYRICS_NOT_FOUND = "LYRICS_NOT_FOUND" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/lyrics/KuGouLyricsProvider.kt b/app/src/main/java/com/zionhuang/music/lyrics/KuGouLyricsProvider.kt new file mode 100644 index 000000000..17a0f1d9d --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/lyrics/KuGouLyricsProvider.kt @@ -0,0 +1,8 @@ +package com.zionhuang.music.lyrics + +import com.zionhuang.kugou.KuGou + +object KuGouLyricsProvider : LyricsProvider { + override suspend fun getLyrics(id: String, title: String, artist: String, duration: Int): Result = + KuGou.getLyrics(title, artist, duration) +} \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/lyrics/LyricsProvider.kt b/app/src/main/java/com/zionhuang/music/lyrics/LyricsProvider.kt new file mode 100644 index 000000000..585cc1617 --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/lyrics/LyricsProvider.kt @@ -0,0 +1,5 @@ +package com.zionhuang.music.lyrics + +interface LyricsProvider { + suspend fun getLyrics(id: String, title: String, artist: String, duration: Int): Result +} \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/lyrics/YouTubeLyricsProvider.kt b/app/src/main/java/com/zionhuang/music/lyrics/YouTubeLyricsProvider.kt new file mode 100644 index 000000000..6973e35fe --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/lyrics/YouTubeLyricsProvider.kt @@ -0,0 +1,14 @@ +package com.zionhuang.music.lyrics + +import com.zionhuang.innertube.YouTube +import com.zionhuang.innertube.models.WatchEndpoint + +object YouTubeLyricsProvider : LyricsProvider { + override suspend fun getLyrics(id: String, title: String, artist: String, duration: Int): Result = + YouTube.next(WatchEndpoint(videoId = id)).mapCatching { nextResult -> + YouTube.browse(nextResult.lyricsEndpoint ?: throw IllegalStateException("Lyrics endpoint not found")).getOrThrow() + }.mapCatching { browseResult -> + browseResult.lyrics ?: throw IllegalStateException("Lyrics unavailable") + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt index 6b1ed15e0..73b4ee873 100644 --- a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt +++ b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt @@ -18,6 +18,8 @@ import android.util.Pair import androidx.core.app.NotificationCompat import androidx.core.content.getSystemService import androidx.core.net.toUri +import androidx.core.util.component1 +import androidx.core.util.component2 import com.google.android.exoplayer2.* import com.google.android.exoplayer2.PlaybackException.* import com.google.android.exoplayer2.Player.* @@ -59,8 +61,13 @@ import com.zionhuang.music.constants.MediaSessionConstants.COMMAND_PLAY_NEXT import com.zionhuang.music.constants.MediaSessionConstants.COMMAND_SEEK_TO_QUEUE_ITEM import com.zionhuang.music.constants.MediaSessionConstants.EXTRA_QUEUE_INDEX import com.zionhuang.music.db.entities.FormatEntity +import com.zionhuang.music.db.entities.LyricsEntity +import com.zionhuang.music.db.entities.LyricsEntity.Companion.LYRICS_NOT_FOUND import com.zionhuang.music.db.entities.Song import com.zionhuang.music.extensions.* +import com.zionhuang.music.lyrics.KuGouLyricsProvider +import com.zionhuang.music.lyrics.LyricsProvider +import com.zionhuang.music.lyrics.YouTubeLyricsProvider import com.zionhuang.music.models.MediaMetadata import com.zionhuang.music.playback.queues.EmptyQueue import com.zionhuang.music.playback.queues.ListQueue @@ -74,8 +81,7 @@ import com.zionhuang.music.utils.InfoCache import com.zionhuang.music.utils.preference.enumPreference import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.* import okhttp3.OkHttpClient import java.io.ObjectInputStream import java.io.ObjectOutputStream @@ -96,8 +102,10 @@ class SongPlayer( private val localRepository = SongRepository private val connectivityManager = context.getSystemService()!! private val bitmapProvider = BitmapProvider(context) + private var autoAddSong by context.preference(R.string.pref_auto_add_song, true) private var audioQuality by enumPreference(context, R.string.pref_audio_quality, AudioQuality.AUTO) + private var currentQueue: Queue = EmptyQueue() val currentMediaMetadata = MutableStateFlow(null) @@ -106,6 +114,9 @@ class SongPlayer( } var currentSong: Song? = null + private val showLyrics = context.sharedPreferences.booleanFlow(context.getString(R.string.pref_show_lyrics), false) + private val lyricProviders: List = listOf(KuGouLyricsProvider, YouTubeLyricsProvider) + val mediaSession = MediaSessionCompat(context, context.getString(R.string.app_name)).apply { isActive = true } @@ -290,6 +301,18 @@ class SongPlayer( setUseFastForwardAction(false) } + private suspend fun loadLyrics(mediaMetadata: MediaMetadata) { + lyricProviders.forEach { provider -> + provider.getLyrics(mediaMetadata.id, mediaMetadata.title, mediaMetadata.artists.joinToString { it.name }, mediaMetadata.duration).onSuccess { lyrics -> + localRepository.upsert(LyricsEntity(mediaMetadata.id, lyrics)) + return + }.onFailure { + it.printStackTrace() + } + } + localRepository.upsert(LyricsEntity(mediaMetadata.id, LYRICS_NOT_FOUND)) + } + init { scope.launch { currentSongFlow.collect { @@ -301,6 +324,16 @@ class SongPlayer( } } } + scope.launch { + combine(currentMediaMetadata.distinctUntilChangedBy { it?.id }, showLyrics) { mediaMetadata, showLyrics -> + Pair(mediaMetadata, showLyrics) + }.collectLatest { pair -> + val (mediaMetadata, showLyrics) = pair + if (showLyrics && mediaMetadata != null && !localRepository.hasLyrics(mediaMetadata.id)) { + loadLyrics(mediaMetadata) + } + } + } if (context.sharedPreferences.getBoolean(context.getString(R.string.pref_persistent_queue), true)) { runCatching { context.filesDir.resolve(PERSISTENT_QUEUE_FILE).inputStream().use { fis -> diff --git a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt index 5d2e2b799..d9aace130 100644 --- a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt +++ b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt @@ -47,6 +47,7 @@ object SongRepository : LocalRepository { private val downloadDao = database.downloadDao private val searchHistoryDao = database.searchHistoryDao private val formatDao = database.formatDao + private val lyricsDao = database.lyricsDao private val connectivityManager = context.getSystemService()!! private var autoDownload by context.preference(R.string.pref_auto_download, false) @@ -700,12 +701,26 @@ object SongRepository : LocalRepository { /** * Format */ - suspend fun getSongFormat(songId: String?): DataWrapper = DataWrapper( + override fun getSongFormat(songId: String?): DataWrapper = DataWrapper( getValueAsync = { withContext(IO) { formatDao.getSongFormat(songId) } }, getFlow = { formatDao.getSongFormatAsFlow(songId) } ) - suspend fun upsert(format: FormatEntity) = withContext(IO) { + override suspend fun upsert(format: FormatEntity) = withContext(IO) { formatDao.upsert(format) } + + /** + * Lyrics + */ + override fun getLyrics(songId: String?): Flow = + lyricsDao.getLyricsAsFlow(songId) + + override suspend fun hasLyrics(songId: String): Boolean = withContext(IO) { + lyricsDao.hasLyrics(songId) + } + + override suspend fun upsert(lyrics: LyricsEntity) = withContext(IO) { + lyricsDao.upsert(lyrics) + } } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/repos/base/LocalRepository.kt b/app/src/main/java/com/zionhuang/music/repos/base/LocalRepository.kt index c769ae0c9..d4f3d254b 100644 --- a/app/src/main/java/com/zionhuang/music/repos/base/LocalRepository.kt +++ b/app/src/main/java/com/zionhuang/music/repos/base/LocalRepository.kt @@ -124,4 +124,17 @@ interface LocalRepository { suspend fun insertSearchHistory(query: String) suspend fun deleteSearchHistory(query: String) suspend fun clearSearchHistory() + + /** + * Format + */ + fun getSongFormat(songId: String?): DataWrapper + suspend fun upsert(format: FormatEntity) + + /** + * Lyrics + */ + fun getLyrics(songId: String?): Flow + suspend fun hasLyrics(songId: String): Boolean + suspend fun upsert(lyrics: LyricsEntity) } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt index 7ccbd9175..741cf5e68 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt @@ -16,6 +16,7 @@ import com.zionhuang.innertube.models.BrowseLocalArtistSongsEndpoint import com.zionhuang.music.R import com.zionhuang.music.constants.MediaSessionConstants.ACTION_TOGGLE_LIKE import com.zionhuang.music.databinding.BottomControlsSheetBinding +import com.zionhuang.music.db.entities.LyricsEntity import com.zionhuang.music.playback.MediaSessionConnection import com.zionhuang.music.ui.activities.MainActivity import com.zionhuang.music.utils.NavigationEndpointHandler @@ -25,6 +26,7 @@ import dev.chrisbanes.insetter.applyInsetter import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch + class BottomControlsFragment : Fragment() { lateinit var binding: BottomControlsSheetBinding private val viewModel by activityViewModels() @@ -78,8 +80,14 @@ class BottomControlsFragment : Fragment() { }) binding.slider.addOnChangeListener { _, value, _ -> binding.position.text = makeTimeString((value).toLong()) + if (sliderIsTracking) { + binding.lyricsView.updateTime(value.toLong(), animate = false) + } + } + binding.lyricsView.setDraggable(true) { time -> + viewModel.mediaController?.transportControls?.seekTo(time) + true } - lifecycleScope.launch { viewModel.playbackState.collect { playbackState -> if (playbackState.state != PlaybackStateCompat.STATE_NONE && playbackState.state != PlaybackStateCompat.STATE_STOPPED) { @@ -89,16 +97,12 @@ class BottomControlsFragment : Fragment() { } } } - lifecycleScope.launch { - viewModel.currentSong.collectLatest { song -> - binding.btnFavorite.setImageResource(if (song?.song?.liked == true) R.drawable.ic_favorite else R.drawable.ic_favorite_border) - } - } lifecycleScope.launch { viewModel.position.collect { position -> if (!sliderIsTracking && binding.slider.isEnabled) { binding.slider.value = position.toFloat().coerceIn(binding.slider.valueFrom, binding.slider.valueTo) binding.position.text = makeTimeString(position) + binding.lyricsView.updateTime(position, animate = true) } } } @@ -114,5 +118,19 @@ class BottomControlsFragment : Fragment() { } } } + lifecycleScope.launch { + viewModel.currentSong.collectLatest { song -> + binding.btnFavorite.setImageResource(if (song?.song?.liked == true) R.drawable.ic_favorite else R.drawable.ic_favorite_border) + } + } + lifecycleScope.launch { + viewModel.currentLyrics.collectLatest { lyrics -> + if (lyrics == null || lyrics.lyrics == LyricsEntity.LYRICS_NOT_FOUND) { + binding.lyricsView.reset() + } else { + binding.lyricsView.loadLyrics(lyrics.lyrics) + } + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/QueueSheetFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/QueueSheetFragment.kt index cba142470..8e0df6375 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/QueueSheetFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/QueueSheetFragment.kt @@ -9,6 +9,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.edit import androidx.core.view.ViewCompat import androidx.core.view.isVisible import androidx.fragment.app.Fragment @@ -127,6 +128,12 @@ class QueueSheetFragment : Fragment() { binding.btnCollapse.setOnClickListener { mainActivity.queueSheetBehavior.state = STATE_COLLAPSED } + binding.btnLyrics.setOnClickListener { + val sharedPreferences = requireContext().sharedPreferences + sharedPreferences.edit { + putBoolean(getString(R.string.pref_show_lyrics), !sharedPreferences.getBoolean(getString(R.string.pref_show_lyrics), false)) + } + } binding.btnAddToLibrary.setOnClickListener { viewModel.transportControls?.sendCustomAction(ACTION_TOGGLE_LIBRARY, null) } diff --git a/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt b/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt new file mode 100644 index 000000000..9dbfad20d --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt @@ -0,0 +1,491 @@ +package com.zionhuang.music.ui.widgets + +import android.animation.ValueAnimator +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.os.Looper +import android.text.Layout +import android.text.StaticLayout +import android.text.TextPaint +import android.text.format.DateUtils.SECOND_IN_MILLIS +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.GestureDetector.SimpleOnGestureListener +import android.view.MotionEvent +import android.view.View +import android.widget.Scroller +import androidx.core.content.ContextCompat +import androidx.core.graphics.withSave +import androidx.interpolator.view.animation.FastOutSlowInInterpolator +import com.zionhuang.music.R +import com.zionhuang.music.utils.lyrics.LyricsEntry +import com.zionhuang.music.utils.lyrics.LyricsUtils +import kotlinx.coroutines.* +import java.lang.Runnable +import kotlin.math.abs + +/** + * Modified from [RetroMusicPlayer](https://github.com/RetroMusicPlayer/RetroMusicPlayer) + */ +@SuppressLint("StaticFieldLeak") +class LyricsView @JvmOverloads constructor( + context: Context?, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : View(context, attrs, defStyleAttr) { + private val lrcEntryList: MutableList = ArrayList() + private val lrcPaint = TextPaint() + private val timePaint = TextPaint() + private lateinit var timeFontMetrics: Paint.FontMetrics + private var dividerHeight = 0f + private var animationDuration: Long = 0 + private var unsyncedTextColor = 0 + private var normalTextColor = 0 + private var normalTextSize = 0f + private var currentTextColor = 0 + private var currentTextSize = 0f + private var animatedCurrentTextSize = 0f + private var isShowTimeline = false + private var timelineTextColor = 0 + private var timelineColor = 0 + private var timeTextColor = 0 + private var drawableWidth = 0 + private var timeTextWidth = 0 + private var defaultLabel: String = "" + private var lrcPadding = 0f + private var onLyricsClickListener: OnLyricsClickListener? = null + private var animator: ValueAnimator? = null + private lateinit var gestureDetector: GestureDetector + private lateinit var scroller: Scroller + private var offset = 0f + private var previousLine = -1 + private var currentLine = 0 + private var inActive = false + private var isTouching = false + private var isFling = false + private var textGravity = 0 // left/center/right + private var isSyncedLyrics = true + private val hideTimelineRunnable = Runnable { + if (hasLyrics() && isSyncedLyrics && inActive) { + inActive = false + smoothScrollTo(currentLine) + } + } + + private val viewScope = CoroutineScope(Dispatchers.Main + Job()) + + private val simpleOnGestureListener = object : SimpleOnGestureListener() { + override fun onDown(e: MotionEvent): Boolean { + if (hasLyrics() && onLyricsClickListener != null) { + if (offset != getOffset(0)) { + parent.requestDisallowInterceptTouchEvent(true) + } + scroller.forceFinished(true) + removeCallbacks(hideTimelineRunnable) + isTouching = true + inActive = true + invalidate() + return true + } + return super.onDown(e) + } + + override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { + if (offset == getOffset(0) && distanceY < 0F) { + return super.onScroll(e1, e2, distanceX, distanceY) + } + if (hasLyrics()) { + offset = (offset - distanceY).coerceIn(getOffset(lrcEntryList.size - 1), getOffset(0)) + invalidate() + parent.requestDisallowInterceptTouchEvent(true) + return true + } + return super.onScroll(e1, e2, distanceX, distanceY) + } + + override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { + if (hasLyrics()) { + scroller.fling(0, offset.toInt(), 0, velocityY.toInt(), 0, 0, getOffset(lrcEntryList.size - 1).toInt(), getOffset(0).toInt()) + isFling = true + return true + } + return super.onFling(e1, e2, velocityX, velocityY) + } + + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { + if (hasLyrics() && isSyncedLyrics) { + val line = getLineByOffset(offset + height / 2 - e.y) + val lineTime = lrcEntryList[line].time + if (onLyricsClickListener?.onLyricsClick(lineTime) == true) { + inActive = false + removeCallbacks(hideTimelineRunnable) + previousLine = if (line != currentLine) line else -1 + currentLine = line + updateCurrentTextSize() + smoothScrollTo(line) + return true + } + } else { + callOnClick() + return true + } + return super.onSingleTapConfirmed(e) + } + } + + private fun init(attrs: AttributeSet?) { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.LyricsView) + currentTextSize = typedArray.getDimension(R.styleable.LyricsView_lrcTextSize, resources.getDimension(R.dimen.lrc_text_size)) + animatedCurrentTextSize = currentTextSize + normalTextSize = typedArray.getDimension(R.styleable.LyricsView_lrcNormalTextSize, resources.getDimension(R.dimen.lrc_text_size)).takeIf { it != 0f } ?: currentTextSize + dividerHeight = typedArray.getDimension(R.styleable.LyricsView_lrcDividerHeight, resources.getDimension(R.dimen.lrc_divider_height)) + val defaultDuration = resources.getInteger(R.integer.lrc_animation_duration) + animationDuration = typedArray.getInt(R.styleable.LyricsView_lrcAnimationDuration, defaultDuration).toLong().takeIf { it > 0 } ?: defaultDuration.toLong() + unsyncedTextColor = typedArray.getColor(R.styleable.LyricsView_lrcUnsyncedTextColor, ContextCompat.getColor(context, R.color.lrc_unsynced_text_color)) + normalTextColor = typedArray.getColor(R.styleable.LyricsView_lrcNormalTextColor, ContextCompat.getColor(context, R.color.lrc_normal_text_color)) + currentTextColor = typedArray.getColor(R.styleable.LyricsView_lrcCurrentTextColor, ContextCompat.getColor(context, R.color.lrc_current_text_color)) + isShowTimeline = typedArray.getBoolean(R.styleable.LyricsView_lrcShowTimeline, false) + timelineTextColor = typedArray.getColor(R.styleable.LyricsView_lrcTimelineTextColor, ContextCompat.getColor(context, R.color.lrc_timeline_text_color)) + defaultLabel = typedArray.getString(R.styleable.LyricsView_lrcLabel).takeUnless { it.isNullOrEmpty() } ?: "Empty" + lrcPadding = typedArray.getDimension(R.styleable.LyricsView_lrcPadding, 0f) + timelineColor = typedArray.getColor(R.styleable.LyricsView_lrcTimelineColor, ContextCompat.getColor(context, R.color.lrc_timeline_color)) + val timelineHeight = typedArray.getDimension(R.styleable.LyricsView_lrcTimelineHeight, resources.getDimension(R.dimen.lrc_timeline_height)) + timeTextColor = typedArray.getColor(R.styleable.LyricsView_lrcTimeTextColor, ContextCompat.getColor(context, R.color.lrc_time_text_color)) + val timeTextSize = typedArray.getDimension(R.styleable.LyricsView_lrcTimeTextSize, resources.getDimension(R.dimen.lrc_time_text_size)) + textGravity = typedArray.getInteger(R.styleable.LyricsView_lrcTextGravity, LyricsEntry.GRAVITY_CENTER) + typedArray.recycle() + drawableWidth = resources.getDimension(R.dimen.lrc_drawable_width).toInt() + timeTextWidth = resources.getDimension(R.dimen.lrc_time_width).toInt() + lrcPaint.apply { + isAntiAlias = true + textSize = currentTextSize + textAlign = Paint.Align.LEFT + } + timePaint.apply { + isAntiAlias = true + textSize = timeTextSize + textAlign = Paint.Align.CENTER + strokeWidth = timelineHeight + strokeCap = Paint.Cap.ROUND + } + timeFontMetrics = timePaint.fontMetrics + gestureDetector = GestureDetector(context, simpleOnGestureListener).apply { + setIsLongpressEnabled(false) + } + scroller = Scroller(context) + } + + fun setNormalColor(normalColor: Int) { + normalTextColor = normalColor + postInvalidate() + } + + fun setCurrentColor(currentColor: Int) { + currentTextColor = currentColor + postInvalidate() + } + + fun setTimelineTextColor(timelineTextColor: Int) { + this.timelineTextColor = timelineTextColor + postInvalidate() + } + + fun setTimelineColor(timelineColor: Int) { + this.timelineColor = timelineColor + postInvalidate() + } + + fun setTimeTextColor(timeTextColor: Int) { + this.timeTextColor = timeTextColor + postInvalidate() + } + + fun setDraggable(draggable: Boolean, onPlayClickListener: OnLyricsClickListener?) { + this.onLyricsClickListener = if (draggable) { + requireNotNull(onPlayClickListener) { "if draggable == true, onPlayClickListener must not be null" } + onPlayClickListener + } else { + null + } + } + + fun setLabel(label: String) { + runOnUi { + defaultLabel = label + invalidate() + } + } + + fun setTextGravity(gravity: Int) { + textGravity = gravity + postInvalidate() + } + + fun loadLyrics(lyrics: String) { + runOnUi { + reset() + viewScope.launch(Dispatchers.IO) { + val entries = if (lyrics.startsWith("[")) { + listOf(LyricsEntry(0L, "")) + LyricsUtils.parseLyrics(lyrics) + } else { + lyrics.lines().mapIndexed { index, line -> LyricsEntry(index * 100L, line) } + } + isSyncedLyrics = lyrics.startsWith("[") + withContext(Dispatchers.Main) { + onLyricsLoaded(entries) + } + } + } + } + + fun hasLyrics(): Boolean = lrcEntryList.isNotEmpty() + + fun updateTime(time: Long, animate: Boolean = true) { + runOnUi { + if (!hasLyrics() || !isSyncedLyrics) { + return@runOnUi + } + val line = findShowLine(time + animationDuration) + if (line != currentLine) { + previousLine = currentLine + currentLine = line + if (!isFling && !inActive) { + smoothScrollTo(line, if (animate) animationDuration else 0) + updateCurrentTextSize(animate) + } else { + invalidate() + } + } + } + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + if (changed) { + initEntryList() + if (hasLyrics()) { + smoothScrollTo(currentLine, 0L) + } + } + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + val centerY = height / 2 + + if (!hasLyrics()) { + lrcPaint.color = currentTextColor + val staticLayout = StaticLayout.Builder.obtain(defaultLabel, 0, defaultLabel.length, lrcPaint, lrcWidth.toInt()) + .setAlignment(Layout.Alignment.ALIGN_CENTER) + .setLineSpacing(0f, 1f) + .setIncludePad(false).build() + drawText(canvas, staticLayout, centerY.toFloat()) + return + } + val centerLine = getCenterLine() + if (inActive && isShowTimeline) { + timePaint.color = timeTextColor + val timeText = LyricsUtils.formatTime(lrcEntryList[centerLine].time) + val timeX = (width - timeTextWidth / 2).toFloat() + val timeY = centerY - (timeFontMetrics.descent + timeFontMetrics.ascent) / 2 + canvas.drawText(timeText, timeX, timeY, timePaint) + } + canvas.translate(0f, offset) + var y = 0f + for (i in lrcEntryList.indices) { + if (i > 0) { + y += (lrcEntryList[i - 1].height + lrcEntryList[i].height) / 2 + dividerHeight + } + if (!isSyncedLyrics) { + lrcPaint.textSize = normalTextSize + lrcPaint.color = unsyncedTextColor + } else if (i == currentLine) { + lrcPaint.textSize = animatedCurrentTextSize + lrcPaint.color = currentTextColor + } else { + lrcPaint.textSize = if (i != previousLine) normalTextSize else currentTextSize - (animatedCurrentTextSize - normalTextSize) + lrcPaint.color = normalTextColor + } + drawText(canvas, lrcEntryList[i].staticLayout!!, y) + } + } + + private fun drawText(canvas: Canvas, staticLayout: StaticLayout, y: Float) { + canvas.withSave { + translate(lrcPadding, y - staticLayout.height / 2) + staticLayout.draw(this) + } + } + + fun updateCurrentTextSize(animate: Boolean = true) { + if (animate) { + ValueAnimator.ofFloat(normalTextSize, currentTextSize).apply { + interpolator = FastOutSlowInInterpolator() + duration = animationDuration + addUpdateListener { + animatedCurrentTextSize = it.animatedValue as Float + invalidate() + } + start() + } + } else { + animatedCurrentTextSize = currentTextSize + } + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) { + isTouching = false + if (hasLyrics() && isSyncedLyrics && !isFling) { + adjustCenter() + postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME) + } + } + return gestureDetector.onTouchEvent(event) + } + + override fun computeScroll() { + if (scroller.computeScrollOffset()) { + offset = scroller.currY.toFloat() + invalidate() + } + if (isFling && scroller.isFinished) { + isFling = false + if (hasLyrics() && isSyncedLyrics && !isTouching) { + adjustCenter() + postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME) + } + } + } + + override fun onDetachedFromWindow() { + removeCallbacks(hideTimelineRunnable) + viewScope.cancel() + super.onDetachedFromWindow() + } + + private fun onLyricsLoaded(entryList: List?) { + if (entryList != null && entryList.isNotEmpty()) { + lrcEntryList.addAll(entryList) + } + lrcEntryList.sort() + initEntryList() + invalidate() + } + + private fun initEntryList() { + if (!hasLyrics() || width == 0) return + lrcEntryList.forEach { lrcEntry -> + lrcEntry.init(lrcPaint, lrcWidth.toInt(), textGravity) + } + offset = (height / 2).toFloat() + } + + fun reset() { + endAnimation() + scroller.forceFinished(true) + inActive = false + isTouching = false + isFling = false + removeCallbacks(hideTimelineRunnable) + lrcEntryList.clear() + offset = 0f + previousLine = -1 + currentLine = 0 + invalidate() + } + + private fun adjustCenter() { + smoothScrollTo(getCenterLine(), ADJUST_DURATION) + } + + private fun smoothScrollTo(line: Int, duration: Long = animationDuration) { + val endOffset = getOffset(line) + endAnimation() + animator = ValueAnimator.ofFloat(offset, endOffset).apply { + this.duration = duration + interpolator = FastOutSlowInInterpolator() + addUpdateListener { valueAnimator -> + offset = valueAnimator.animatedValue as Float + invalidate() + } + start() + } + } + + private fun endAnimation() { + if (animator != null && animator!!.isRunning) { + animator!!.end() + } + } + + private fun findShowLine(time: Long): Int { + var left = 0 + var right = lrcEntryList.size + while (left <= right) { + val middle = (left + right) / 2 + val middleTime = lrcEntryList[middle].time + if (time < middleTime) { + right = middle - 1 + } else { + if (middle + 1 >= lrcEntryList.size || time < lrcEntryList[middle + 1].time) { + return middle + } + left = middle + 1 + } + } + return 0 + } + + private fun getCenterLine() = getLineByOffset(offset) + private fun getLineByOffset(offset: Float): Int { + var line = 0 + var minDistance = Float.MAX_VALUE + for (i in lrcEntryList.indices) { + if (abs(offset - getOffset(i)) < minDistance) { + minDistance = abs(offset - getOffset(i)) + line = i + } + } + return line + } + + private fun getOffset(line: Int): Float { + if (lrcEntryList.isEmpty()) return 0F + if (lrcEntryList[line].offset == Float.MIN_VALUE) { + var offset = (height / 2).toFloat() + for (i in 1..line) { + offset -= (lrcEntryList[i - 1].height + lrcEntryList[i].height) / 2 + dividerHeight + } + lrcEntryList[line].offset = offset + } + return lrcEntryList[line].offset + } + + private val lrcWidth: Float + get() = width - lrcPadding * 2 + + private fun runOnUi(r: Runnable) { + if (Looper.myLooper() == Looper.getMainLooper()) { + r.run() + } else { + post(r) + } + } + + fun interface OnLyricsClickListener { + fun onLyricsClick(time: Long): Boolean + } + + companion object { + private const val ADJUST_DURATION = 300L + private const val TIMELINE_KEEP_TIME = 4 * SECOND_IN_MILLIS + } + + init { + init(attrs) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsEntry.kt b/app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsEntry.kt new file mode 100644 index 000000000..1f66dc59f --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsEntry.kt @@ -0,0 +1,37 @@ +package com.zionhuang.music.utils.lyrics + +import android.text.Layout +import android.text.StaticLayout +import android.text.TextPaint + +data class LyricsEntry( + val time: Long, + val text: String, +) : Comparable { + var staticLayout: StaticLayout? = null + private set + var offset = Float.MIN_VALUE // distance to the top of [LyricsView] + val height: Int + get() = staticLayout?.height ?: 0 + + fun init(paint: TextPaint, width: Int, gravity: Int) { + staticLayout = StaticLayout.Builder.obtain(text, 0, text.length, paint, width) + .setAlignment(when (gravity) { + GRAVITY_LEFT -> Layout.Alignment.ALIGN_NORMAL + GRAVITY_CENTER -> Layout.Alignment.ALIGN_CENTER + GRAVITY_RIGHT -> Layout.Alignment.ALIGN_OPPOSITE + else -> Layout.Alignment.ALIGN_CENTER + }) + .setLineSpacing(0f, 1f) + .setIncludePad(false).build() + offset = Float.MIN_VALUE + } + + override fun compareTo(other: LyricsEntry): Int = (time - other.time).toInt() + + companion object { + const val GRAVITY_CENTER = 0 + const val GRAVITY_LEFT = 1 + const val GRAVITY_RIGHT = 2 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsUtils.kt b/app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsUtils.kt new file mode 100644 index 000000000..2137c0d65 --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsUtils.kt @@ -0,0 +1,46 @@ +package com.zionhuang.music.utils.lyrics + +import android.text.format.DateUtils.MINUTE_IN_MILLIS +import android.text.format.DateUtils.SECOND_IN_MILLIS +import java.util.* + +object LyricsUtils { + private val LINE_REGEX = "((\\[\\d\\d:\\d\\d\\.\\d{2,3}\\])+)(.+)".toRegex() + private val TIME_REGEX = "\\[(\\d\\d):(\\d\\d)\\.(\\d{2,3})\\]".toRegex() + + fun parseLyrics(lyrics: String): List = + lyrics.lines() + .flatMap { line -> + parseLine(line).orEmpty() + }.sorted() + + private fun parseLine(line: String): List? { + if (line.isEmpty()) { + return null + } + val matchResult = LINE_REGEX.matchEntire(line.trim()) ?: return null + val times = matchResult.groupValues[1] + val text = matchResult.groupValues[3] + val timeMatchResults = TIME_REGEX.findAll(times) + + return timeMatchResults.map { timeMatchResult -> + val min = timeMatchResult.groupValues[1].toLong() + val sec = timeMatchResult.groupValues[2].toLong() + val milString = timeMatchResult.groupValues[3] + var mil = milString.toLong() + if (milString.length == 2) { + mil *= 10 + } + val time = min * MINUTE_IN_MILLIS + sec * SECOND_IN_MILLIS + mil + LyricsEntry(time, text) + }.toList() + } + + fun formatTime(milli: Long): String { + val m = (milli / MINUTE_IN_MILLIS).toInt() + val s = (milli / SECOND_IN_MILLIS % 60).toInt() + val mm = String.format(Locale.getDefault(), "%02d", m) + val ss = String.format(Locale.getDefault(), "%02d", s) + return "$mm:$ss" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/PlaybackViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/PlaybackViewModel.kt index 58a64babb..2bb515552 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/PlaybackViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/PlaybackViewModel.kt @@ -3,11 +3,14 @@ package com.zionhuang.music.viewmodels import android.app.Activity import android.app.Application import android.support.v4.media.MediaMetadataCompat +import android.support.v4.media.MediaMetadataCompat.METADATA_KEY_MEDIA_ID import android.support.v4.media.session.MediaControllerCompat import android.support.v4.media.session.MediaControllerCompat.TransportControls import android.support.v4.media.session.PlaybackStateCompat.* import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import com.zionhuang.music.R +import com.zionhuang.music.extensions.preferenceLiveData import com.zionhuang.music.models.PlaybackStateData import com.zionhuang.music.playback.MediaSessionConnection import com.zionhuang.music.playback.queues.Queue @@ -32,11 +35,15 @@ class PlaybackViewModel(application: Application) : AndroidViewModel(application val queueItems = MediaSessionConnection.queueItems val currentSong = mediaMetadata.flatMapLatest { mediaMetadata -> - SongRepository.getSongById(mediaMetadata?.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID)).flow + SongRepository.getSongById(mediaMetadata?.getString(METADATA_KEY_MEDIA_ID)).flow } val currentSongFormat = mediaMetadata.flatMapLatest { mediaMetadata -> - SongRepository.getSongFormat(mediaMetadata?.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID)).getFlow() + SongRepository.getSongFormat(mediaMetadata?.getString(METADATA_KEY_MEDIA_ID)).getFlow() } + val currentLyrics = mediaMetadata.flatMapLatest { mediaMetadata -> + SongRepository.getLyrics(mediaMetadata?.getString(METADATA_KEY_MEDIA_ID)) + }.stateIn(viewModelScope, SharingStarted.Lazily, null) + val showLyrics = preferenceLiveData(R.string.pref_show_lyrics, false) val position = MutableStateFlow(0L) val duration = MutableStateFlow(0L) diff --git a/app/src/main/res/drawable/ic_lyrics.xml b/app/src/main/res/drawable/ic_lyrics.xml new file mode 100644 index 000000000..cfeea76f2 --- /dev/null +++ b/app/src/main/res/drawable/ic_lyrics.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/layout-land/bottom_controls_sheet.xml b/app/src/main/res/layout-land/bottom_controls_sheet.xml index c9d5d9396..7ba2badfd 100644 --- a/app/src/main/res/layout-land/bottom_controls_sheet.xml +++ b/app/src/main/res/layout-land/bottom_controls_sheet.xml @@ -70,7 +70,7 @@ android:layout_height="match_parent" android:alpha=".75" android:background="#000000" - android:visibility="@{viewModel.playbackState.errorCode != 0 ? View.VISIBLE : View.GONE}" + android:visibility="@{(viewModel.playbackState.errorCode != 0 || viewModel.showLyrics) ? View.VISIBLE : View.GONE}" tools:visibility="gone" /> + + diff --git a/app/src/main/res/layout/bottom_controls_sheet.xml b/app/src/main/res/layout/bottom_controls_sheet.xml index 16dfa36ee..4dbf15238 100644 --- a/app/src/main/res/layout/bottom_controls_sheet.xml +++ b/app/src/main/res/layout/bottom_controls_sheet.xml @@ -60,7 +60,7 @@ android:layout_height="match_parent" android:alpha=".75" android:background="#000000" - android:visibility="@{viewModel.playbackState.errorCode != 0 ? View.VISIBLE : View.GONE}" + android:visibility="@{(viewModel.playbackState.errorCode != 0 || viewModel.showLyrics) ? View.VISIBLE : View.GONE}" tools:visibility="gone" /> + + diff --git a/app/src/main/res/layout/queue_sheet.xml b/app/src/main/res/layout/queue_sheet.xml index c0b616ca1..d808555df 100644 --- a/app/src/main/res/layout/queue_sheet.xml +++ b/app/src/main/res/layout/queue_sheet.xml @@ -125,6 +125,18 @@ android:src="@drawable/ic_queue_music" tools:ignore="SpeakableTextPresentCheck" /> + + All songs Searched songs + + + Lyrics not found diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index a911381cb..ed55f0e29 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -281,4 +281,7 @@ All songs Searched songs + + + Lyrics not found \ No newline at end of file diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index 7d69ed115..c71dbccf7 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -281,4 +281,7 @@ All songs Searched songs + + + Lyrics not found diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index aef85487f..3be8b6102 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -281,4 +281,7 @@ All songs Searched songs + + + Lyrics not found diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 6b31c33a9..9fe1b4904 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -285,4 +285,7 @@ All songs Searched songs + + + Lyrics not found diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index e04f10c32..ece465f14 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -281,4 +281,7 @@ Minden dal Keresett dalok + + + Lyrics not found diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 0d646cdfd..362350979 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -281,4 +281,7 @@ Tutti i brani Brani cercati + + + Lyrics not found diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 5ed621c16..66f369b93 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -275,4 +275,7 @@ All songs Searched songs + + + Lyrics not found diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 9a2e15696..2deae2ff3 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -275,4 +275,7 @@ All songs Searched songs + + + Lyrics not found diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml index d43dca81d..c5664fd92 100644 --- a/app/src/main/res/values-ml-rIN/strings.xml +++ b/app/src/main/res/values-ml-rIN/strings.xml @@ -281,4 +281,7 @@ All songs Searched songs + + + Lyrics not found \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 4bc4dcf23..8cb264fa6 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -281,4 +281,7 @@ Todas as músicas Músicas pesquisadas + + + Lyrics not found diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 3a8df6b69..0da39182a 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -281,4 +281,7 @@ All songs Searched songs + + + Lyrics not found diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 5a255837f..b8372d36c 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -275,4 +275,7 @@ 全部歌曲 搜索的歌曲 + + + Lyrics not found diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index f4a17bc3f..8c99c1531 100755 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -275,4 +275,7 @@ 全部歌曲 搜尋的歌曲 + + + 沒有歌詞 diff --git a/app/src/main/res/values/lyrics_colors.xml b/app/src/main/res/values/lyrics_colors.xml new file mode 100644 index 000000000..a558eb259 --- /dev/null +++ b/app/src/main/res/values/lyrics_colors.xml @@ -0,0 +1,9 @@ + + + #E6E1E5 + #CAC4D0 + #6750A4 + #CAC4D0 + #CAC4D0 + #CAC4D0 + \ No newline at end of file diff --git a/app/src/main/res/values/lyrics_dimens.xml b/app/src/main/res/values/lyrics_dimens.xml new file mode 100644 index 000000000..fae7fdc9e --- /dev/null +++ b/app/src/main/res/values/lyrics_dimens.xml @@ -0,0 +1,10 @@ + + + 300 + 20sp + 16sp + 16dp + 1dp + 30dp + 40dp + \ No newline at end of file diff --git a/app/src/main/res/values/lyrics_view_attrs.xml b/app/src/main/res/values/lyrics_view_attrs.xml new file mode 100644 index 000000000..31c137907 --- /dev/null +++ b/app/src/main/res/values/lyrics_view_attrs.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2b7cfe99c..f608b85ef 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -280,4 +280,7 @@ All songs Searched songs + + + Lyrics not found diff --git a/innertube/build.gradle.kts b/innertube/build.gradle.kts index 3b25c7d38..191bc84e6 100644 --- a/innertube/build.gradle.kts +++ b/innertube/build.gradle.kts @@ -59,13 +59,13 @@ protobuf { } } -val ktor_version: String by project -val logback_version: String by project - tasks.withType().configureEach { kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" } +val ktor_version: String by project +val logback_version: String by project + dependencies { implementation("io.ktor:ktor-client-core:$ktor_version") implementation("io.ktor:ktor-client-okhttp:$ktor_version") diff --git a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt index ee4f0d6f3..d617bef2c 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt @@ -11,6 +11,7 @@ import java.net.Proxy /** * Parse useful data with [InnerTube] sending requests. + * Modified from [ViMusic](https://github.com/vfsfitvnm/ViMusic) */ object YouTube { private val innerTube = InnerTube() diff --git a/kugou/.gitignore b/kugou/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/kugou/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/kugou/build.gradle.kts b/kugou/build.gradle.kts new file mode 100644 index 000000000..2da1ea611 --- /dev/null +++ b/kugou/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + kotlin("jvm") + id("kotlinx-serialization") +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType().configureEach { + kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" +} + +val ktor_version: String by project + +dependencies { + implementation("io.ktor:ktor-client-core:$ktor_version") + implementation("io.ktor:ktor-client-okhttp:$ktor_version") + implementation("io.ktor:ktor-client-content-negotiation:$ktor_version") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") + implementation("io.ktor:ktor-client-encoding:$ktor_version") + implementation("io.ktor:ktor-client-logging-jvm:$ktor_version") + // SC to TC + implementation("com.github.houbb:opencc4j:1.7.2") + testImplementation("junit:junit:4.13.2") +} \ No newline at end of file diff --git a/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt b/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt new file mode 100644 index 000000000..63f4c3ec2 --- /dev/null +++ b/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt @@ -0,0 +1,157 @@ +package com.zionhuang.kugou + +import com.github.houbb.opencc4j.util.ZhConverterUtil +import com.zionhuang.kugou.models.DownloadLyricsResponse +import com.zionhuang.kugou.models.SearchLyricsResponse +import com.zionhuang.kugou.models.SearchSongResponse +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.okhttp.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.compression.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.util.* +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import java.lang.Character.UnicodeScript +import kotlin.math.abs + +/** + * KuGou Lyrics Library + * Modified from [ViMusic](https://github.com/vfsfitvnm/ViMusic) + */ +object KuGou { + var useTraditionalChinese: Boolean = false + + @OptIn(ExperimentalSerializationApi::class) + private val client = HttpClient(OkHttp) { + expectSuccess = true + + install(ContentNegotiation) { + val json = Json { + ignoreUnknownKeys = true + explicitNulls = false + encodeDefaults = true + } + json(json) + json(json, ContentType.Text.Html) + json(json, ContentType.Text.Plain) + } + + install(ContentEncoding) { + gzip() + deflate() + } + + defaultRequest { + url("https://krcs.kugou.com") + } + } + + suspend fun getLyrics(title: String, artist: String, duration: Int): Result = runCatching { + getLyricsCandidate(title, artist, duration)?.let { candidate -> + downloadLyrics(candidate.id, candidate.accesskey).content.decodeBase64String().normalize(normalizeTitle(title), normalizeArtist(artist)) + } ?: throw IllegalStateException("No lyrics candidate") + } + + suspend fun getLyricsCandidate(title: String, artist: String, duration: Int): SearchLyricsResponse.Candidate? { + val keyword = generateKeyword(title, artist) + searchSongs(keyword).data.info.forEach { song -> + if (abs(song.duration - duration) <= 8) { + val candidate = searchLyricsByHash(song.hash).candidates.firstOrNull() + if (candidate != null) return candidate + } + } + return searchLyricsByKeyword(keyword, duration).candidates.firstOrNull() + } + + private suspend fun searchSongs(keyword: String) = client.get("https://mobileservice.kugou.com/api/v3/search/song") { + parameter("version", 9108) + parameter("plat", 0) + parameter("keyword", keyword) + }.body() + + private suspend fun searchLyricsByKeyword(keyword: String, duration: Int) = client.get("https://lyrics.kugou.com/search") { + parameter("ver", 1) + parameter("man", "yes") + parameter("client", "pc") + parameter("keyword", keyword) + parameter("duration", duration * 1000) + }.body() + + private suspend fun searchLyricsByHash(hash: String) = client.get("https://lyrics.kugou.com/search") { + parameter("ver", 1) + parameter("man", "yes") + parameter("client", "pc") + parameter("hash", hash) + }.body() + + private suspend fun downloadLyrics(id: Long, accessKey: String) = client.get("https://lyrics.kugou.com/download") { + parameter("fmt", "lrc") + parameter("charset", "utf8") + parameter("client", "pc") + parameter("ver", 1) + parameter("id", id) + parameter("accesskey", accessKey) + }.body() + + private fun normalizeTitle(title: String) = title + .replace("\\(.*\\)".toRegex(), "") + .replace("(.*)".toRegex(), "") + .replace("「.*」".toRegex(), "") + .replace("『.*』".toRegex(), "") + .replace("<.*>".toRegex(), "") + .replace("《.*》".toRegex(), "") + .replace("〈.*〉".toRegex(), "") + .replace("<.*>".toRegex(), "") + + private fun normalizeArtist(artist: String) = artist + .replace(", ", "、") + .replace(" & ", "、") + .replace(".", "") + .replace("和", "、") + .replace("\\(.*\\)".toRegex(), "") + .replace("(.*)".toRegex(), "") + + private fun generateKeyword(title: String, artist: String): String { + return "${normalizeTitle(title)} - ${normalizeArtist(artist)}" + } + + private fun String.toSimplifiedChinese() = ZhConverterUtil.toSimple(this) + private fun String.toTraditionalChinese(useTraditionalChinese: Boolean) = if (useTraditionalChinese) ZhConverterUtil.toTraditional(this) else this + + private fun String.normalize(title: String, artist: String): String = lines().filterNot { line -> + line.endsWith("]") || BANNED_WORDS.any { line.contains(it) } || line.endsWith("].") + }.let { lines -> + val firstLine = lines.firstOrNull()?.toSimplifiedChinese() ?: return@let lines + if (title.toSimplifiedChinese() in firstLine || + artist.split("、").any { + it.toSimplifiedChinese() in firstLine + } + ) { + lines.drop(1) + } else lines + }.joinToString(separator = "\n").toTraditionalChinese(useTraditionalChinese && none { c -> + UnicodeScript.of(c.code) in JapaneseUnicodeScript + }) + + private val BANNED_WORDS = listOf( + "]词:", "]词:", "]作词:", "]作词:", + "]曲:", "]曲:", "]作曲:", "]作曲:", + "]编曲:", "]编曲 Arrangement:", + "]Producer:", "]制作人 Producer:", + "]Drums:", + "]Guitar:", + "]Strings:", + "]Mixer:", + "]Mastering Engineer:" + ) + + private val JapaneseUnicodeScript = hashSetOf( + UnicodeScript.HIRAGANA, + UnicodeScript.KATAKANA, + ) +} diff --git a/kugou/src/main/java/com/zionhuang/kugou/models/DownloadLyricsResponse.kt b/kugou/src/main/java/com/zionhuang/kugou/models/DownloadLyricsResponse.kt new file mode 100644 index 000000000..380ff260b --- /dev/null +++ b/kugou/src/main/java/com/zionhuang/kugou/models/DownloadLyricsResponse.kt @@ -0,0 +1,8 @@ +package com.zionhuang.kugou.models + +import kotlinx.serialization.Serializable + +@Serializable +data class DownloadLyricsResponse( + val content: String, +) diff --git a/kugou/src/main/java/com/zionhuang/kugou/models/SearchLyricsResponse.kt b/kugou/src/main/java/com/zionhuang/kugou/models/SearchLyricsResponse.kt new file mode 100644 index 000000000..2ab310d4d --- /dev/null +++ b/kugou/src/main/java/com/zionhuang/kugou/models/SearchLyricsResponse.kt @@ -0,0 +1,23 @@ +package com.zionhuang.kugou.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SearchLyricsResponse( + val status: Int, + val info: String, + val errcode: Int, + val errmsg: String, + val expire: Int, + val candidates: List, +) { + @Serializable + data class Candidate( + val id: Long, + @SerialName("product_from") + val productFrom: String, // Consider choosing '官方推荐歌词' + val duration: Long, + val accesskey: String, + ) +} diff --git a/kugou/src/main/java/com/zionhuang/kugou/models/SearchSongResponse.kt b/kugou/src/main/java/com/zionhuang/kugou/models/SearchSongResponse.kt new file mode 100644 index 000000000..94d12081c --- /dev/null +++ b/kugou/src/main/java/com/zionhuang/kugou/models/SearchSongResponse.kt @@ -0,0 +1,22 @@ +package com.zionhuang.kugou.models + +import kotlinx.serialization.Serializable + +@Serializable +data class SearchSongResponse( + val status: Int, + val errcode: Int, + val error: String, + val data: Data, +) { + @Serializable + data class Data( + val info: List, + ) { + @Serializable + data class Info( + val duration: Int, + val hash: String, + ) + } +} \ No newline at end of file diff --git a/kugou/src/test/java/Test.kt b/kugou/src/test/java/Test.kt new file mode 100644 index 000000000..e4f49e3eb --- /dev/null +++ b/kugou/src/test/java/Test.kt @@ -0,0 +1,15 @@ +import com.zionhuang.kugou.KuGou +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertTrue +import org.junit.Test + +class Test { + @Test + fun test() = runBlocking { + val candidates = KuGou.getLyricsCandidate("千年以後 (After A Thousand Years)", "陳零九", 285) + assertTrue(candidates != null) + val lyrics = KuGou.getLyrics("楊丞琳", "點水", 259).getOrThrow() + println(lyrics) + assertTrue(lyrics != null) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index d59de0b4a..325ee702a 100755 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,4 @@ rootProject.name = "Music" include(":app") include(":innertube") +include(":kugou") From adf8a6f62ed2fe2b143a465c201ac740d51af82e Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sun, 23 Oct 2022 12:54:13 +0800 Subject: [PATCH 19/95] Add option to align lyrics left/center/right (#76) --- .../music/ui/fragments/BottomControlsFragment.kt | 5 +++++ .../com/zionhuang/music/ui/widgets/LyricsView.kt | 1 + .../com/zionhuang/music/utils/lyrics/LyricsEntry.kt | 4 ++-- app/src/main/res/values-es-rUS/strings.xml | 6 +++++- app/src/main/res/values-es/strings.xml | 6 +++++- app/src/main/res/values-fa-rIR/strings.xml | 6 +++++- app/src/main/res/values-fi-rFI/strings.xml | 6 +++++- app/src/main/res/values-fr-rFR/strings.xml | 4 ++++ app/src/main/res/values-hu/strings.xml | 6 +++++- app/src/main/res/values-it/strings.xml | 6 +++++- app/src/main/res/values-ja-rJP/strings.xml | 6 +++++- app/src/main/res/values-ko-rKR/strings.xml | 6 +++++- app/src/main/res/values-ml-rIN/strings.xml | 6 +++++- app/src/main/res/values-pt-rBR/strings.xml | 6 +++++- app/src/main/res/values-sv-rSE/strings.xml | 6 +++++- app/src/main/res/values-zh-rCN/strings.xml | 6 +++++- app/src/main/res/values-zh-rTW/strings.xml | 6 +++++- app/src/main/res/values/constants.xml | 13 +++++++++++++ app/src/main/res/values/strings.xml | 6 +++++- app/src/main/res/xml/pref_appearance.xml | 8 ++++++++ 20 files changed, 103 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt index 741cf5e68..57360813a 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt @@ -21,6 +21,7 @@ import com.zionhuang.music.playback.MediaSessionConnection import com.zionhuang.music.ui.activities.MainActivity import com.zionhuang.music.utils.NavigationEndpointHandler import com.zionhuang.music.utils.makeTimeString +import com.zionhuang.music.utils.preference.PreferenceLiveData import com.zionhuang.music.viewmodels.PlaybackViewModel import dev.chrisbanes.insetter.applyInsetter import kotlinx.coroutines.flow.collectLatest @@ -88,6 +89,10 @@ class BottomControlsFragment : Fragment() { viewModel.mediaController?.transportControls?.seekTo(time) true } + PreferenceLiveData(requireContext(), R.string.pref_lyrics_text_position, "1").observe(viewLifecycleOwner) { + binding.lyricsView.setTextGravity(it.toIntOrNull() ?: 1) + binding.lyricsView.invalidate() + } lifecycleScope.launch { viewModel.playbackState.collect { playbackState -> if (playbackState.state != PlaybackStateCompat.STATE_NONE && playbackState.state != PlaybackStateCompat.STATE_STOPPED) { diff --git a/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt b/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt index 9dbfad20d..708a614ec 100644 --- a/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt +++ b/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt @@ -220,6 +220,7 @@ class LyricsView @JvmOverloads constructor( fun setTextGravity(gravity: Int) { textGravity = gravity + initEntryList() postInvalidate() } diff --git a/app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsEntry.kt b/app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsEntry.kt index 1f66dc59f..926ccc933 100644 --- a/app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsEntry.kt +++ b/app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsEntry.kt @@ -30,8 +30,8 @@ data class LyricsEntry( override fun compareTo(other: LyricsEntry): Int = (time - other.time).toInt() companion object { - const val GRAVITY_CENTER = 0 - const val GRAVITY_LEFT = 1 + const val GRAVITY_LEFT = 0 + const val GRAVITY_CENTER = 1 const val GRAVITY_RIGHT = 2 } } \ No newline at end of file diff --git a/app/src/main/res/values-es-rUS/strings.xml b/app/src/main/res/values-es-rUS/strings.xml index c0157ff1d..1a702ff56 100644 --- a/app/src/main/res/values-es-rUS/strings.xml +++ b/app/src/main/res/values-es-rUS/strings.xml @@ -17,9 +17,13 @@ Modo oscuro Encendido Apagado + Predeterminado del sistema Default open tab Customize navigation tabs - Predeterminado del sistema + Lyrics text position + Left + Center + Right Contenido Idioma por defecto del contenido diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ed55f0e29..3ddea90dc 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -17,9 +17,13 @@ Tema oscuro Encendido Apagado + Seguir el sistema Default open tab Customize navigation tabs - Seguir el sistema + Lyrics text position + Left + Center + Right Contenido Idioma de contenido predeterminado diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index c71dbccf7..ee394f590 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -17,9 +17,13 @@ تم تاریک روشن خاموش + پیروی از سیستم زبانه باز پیش‌فرض Customize navigation tabs - پیروی از سیستم + Lyrics text position + Left + Center + Right محتوا زبان پیش‌فرض محتوا diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 3be8b6102..c3d21c4e6 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -17,9 +17,13 @@ Tumma teema Käytössä Pois käytöstä + Järjestelmän teeman mukainen Default open tab Customize navigation tabs - Järjestelmän teeman mukainen + Lyrics text position + Left + Center + Right Sisältö Sisällön oletuskieli diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 9fe1b4904..893b10307 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -17,9 +17,13 @@ Dark theme On Off + Lyrics text position Default open tab Customize navigation tabs Follow system + Left + Center + Right Content Langue du contenu par défaut diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index ece465f14..3075a040d 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -17,9 +17,13 @@ Sötét téma Be Ki + Rendszer szerint Alapért. nyitott lap A navigációs lapok testreszabása - Rendszer szerint + Lyrics text position + Left + Center + Right Tartalom A tartalom alapért. nyelve diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 362350979..6e5ebe5bb 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -17,9 +17,13 @@ Tema scuro Attivato Disattivato + Segui sistema Scheda principale predefinita Personalizza schede di navigazione - Segui sistema + Lyrics text position + Left + Center + Right Contenuti Lingua predefinita dei contenuti diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 66f369b93..60917c1f6 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -17,9 +17,13 @@ ダークテーマ オン オフ + システムに従う 起動時に開くタブ Customize navigation tabs - システムに従う + Lyrics text position + Left + Center + Right コンテンツ デフォルトのコンテンツの言語 diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 2deae2ff3..f478b52c4 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -17,9 +17,13 @@ 다크 테마 + 시스템 Default open tab Customize navigation tabs - 시스템 + Lyrics text position + Left + Center + Right 콘텐츠 기본 콘텐츠 언어 diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml index c5664fd92..f656f1178 100644 --- a/app/src/main/res/values-ml-rIN/strings.xml +++ b/app/src/main/res/values-ml-rIN/strings.xml @@ -17,9 +17,13 @@ ഡാർക്ക് തീം ഓൺ ഓഫ് + സിസ്റ്റം പിന്തുടരുക സ്ഥിര ഓപ്പൺ ടാബ് Customize navigation tabs - സിസ്റ്റം പിന്തുടരുക + Lyrics text position + Left + Center + Right കന്റെന്റ് സ്ഥിര കന്റെന്റ് ഭാഷ diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 8cb264fa6..6c3469c35 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -17,9 +17,13 @@ Tema escuro On Off + Seguir o sistema Aba padrão ao iniciar Costumar barra de navegação - Seguir o sistema + Lyrics text position + Left + Center + Right Conteúdo Idioma padrão do conteúdo diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 0da39182a..cfeef0c69 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -17,9 +17,13 @@ Mörkt tema Av + Följ systemet Default open tab Customize navigation tabs - Följ systemet + Lyrics text position + Left + Center + Right Innehåll Standard innehållsspråk diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b8372d36c..73bee1ab5 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -17,9 +17,13 @@ 深色主题 + 跟随系统 默认启动选项卡 自定义导航选项卡 - 跟随系统 + Lyrics text position + Left + Center + Right 内容 默认内容语言 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 8c99c1531..2f3147899 100755 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -17,9 +17,13 @@ 深色主題 + 跟隨系統 預設啟動標籤 自訂導覽列 - 跟隨系統 + 歌詞文字位置 + 靠左 + 置中 + 靠右 內容 預設內容語言 diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index 111acb1f1..385104b57 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -23,6 +23,7 @@ DARK_THEME DEFAULT_OPEN_TAB NAV_TAB_CONFIG + LRC_TEXT_POS CONTENT_LANGUAGE CONTENT_COUNTRY @@ -139,6 +140,18 @@ 4 + + @string/align_left + @string/align_center + @string/align_right + + + + 0 + 1 + 2 + + HTTP SOCKS diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f608b85ef..3e2eefa15 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,9 +17,13 @@ Dark theme On Off + Follow system Default open tab Customize navigation tabs - Follow system + Lyrics text position + Left + Center + Right Content Default content language diff --git a/app/src/main/res/xml/pref_appearance.xml b/app/src/main/res/xml/pref_appearance.xml index dc699902c..2be3ed665 100644 --- a/app/src/main/res/xml/pref_appearance.xml +++ b/app/src/main/res/xml/pref_appearance.xml @@ -32,4 +32,12 @@ android:icon="@drawable/ic_view_column" android:key="@string/pref_nav_tab_config" android:title="@string/pref_customize_navigation_tabs" /> + \ No newline at end of file From 1b13d1da65f07905bea811cba1f396690a41ea0f Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sun, 23 Oct 2022 13:07:07 +0800 Subject: [PATCH 20/95] Keep screen on when showing lyrics (#76) --- .../java/com/zionhuang/music/extensions/ActivityExt.kt | 9 +++++++++ .../com/zionhuang/music/ui/activities/MainActivity.kt | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/app/src/main/java/com/zionhuang/music/extensions/ActivityExt.kt b/app/src/main/java/com/zionhuang/music/extensions/ActivityExt.kt index cf3b25734..9346709af 100644 --- a/app/src/main/java/com/zionhuang/music/extensions/ActivityExt.kt +++ b/app/src/main/java/com/zionhuang/music/extensions/ActivityExt.kt @@ -1,6 +1,7 @@ package com.zionhuang.music.extensions import android.app.Activity +import android.view.WindowManager import androidx.annotation.DimenRes import androidx.annotation.IdRes import androidx.appcompat.app.AppCompatActivity @@ -18,4 +19,12 @@ fun AppCompatActivity.replaceFragment(@IdRes id: Int, fragment: Fragment, tag: S fun Activity.dip(@DimenRes id: Int): Int { return resources.getDimensionPixelSize(id) +} + +fun AppCompatActivity.keepScreenOn(keepScreenOn: Boolean) { + if (keepScreenOn) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt b/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt index 2db134c8f..34e5a48a8 100644 --- a/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt +++ b/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt @@ -158,6 +158,9 @@ class MainActivity : ThemedBindingActivity(), NavController lifecycleScope.launch { SongRepository.validateDownloads() } + preferenceLiveData(R.string.pref_show_lyrics, false).observe(this) { showLyrics -> + keepScreenOn(showLyrics && bottomSheetBehavior.state == STATE_EXPANDED) + } lifecycleScope.launch { AdaptiveUtils.orientation.collectLatest { orientation -> binding.container.updateLayoutParams { @@ -203,6 +206,11 @@ class MainActivity : ThemedBindingActivity(), NavController if (newState == STATE_HIDDEN) { MediaSessionConnection.mediaController?.transportControls?.stop() } + if (newState == STATE_COLLAPSED || newState == STATE_HIDDEN) { + keepScreenOn(false) + } else if (newState == STATE_EXPANDED && sharedPreferences.getBoolean(getString(R.string.pref_show_lyrics), false)) { + keepScreenOn(true) + } } private fun onBottomSheetSlide(slideOffset: Float) { From 75d935e7703187dc476f9866a0d6afc6751b1860 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sun, 23 Oct 2022 13:15:07 +0800 Subject: [PATCH 21/95] Add option to disable KuGou lyrics provider (#76) --- .../zionhuang/music/lyrics/KuGouLyricsProvider.kt | 6 ++++++ .../com/zionhuang/music/lyrics/LyricsProvider.kt | 3 +++ .../zionhuang/music/lyrics/YouTubeLyricsProvider.kt | 2 ++ .../java/com/zionhuang/music/playback/SongPlayer.kt | 12 +++++++----- app/src/main/res/values-es-rUS/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fa-rIR/strings.xml | 1 + app/src/main/res/values-fi-rFI/strings.xml | 1 + app/src/main/res/values-fr-rFR/strings.xml | 1 + app/src/main/res/values-hu/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-ja-rJP/strings.xml | 1 + app/src/main/res/values-ko-rKR/strings.xml | 1 + app/src/main/res/values-ml-rIN/strings.xml | 1 + app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-sv-rSE/strings.xml | 1 + app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/constants.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/pref_privacy.xml | 5 +++++ 21 files changed, 39 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/lyrics/KuGouLyricsProvider.kt b/app/src/main/java/com/zionhuang/music/lyrics/KuGouLyricsProvider.kt index 17a0f1d9d..f2f6ccddd 100644 --- a/app/src/main/java/com/zionhuang/music/lyrics/KuGouLyricsProvider.kt +++ b/app/src/main/java/com/zionhuang/music/lyrics/KuGouLyricsProvider.kt @@ -1,8 +1,14 @@ package com.zionhuang.music.lyrics +import android.content.Context import com.zionhuang.kugou.KuGou +import com.zionhuang.music.R +import com.zionhuang.music.extensions.sharedPreferences object KuGouLyricsProvider : LyricsProvider { + override fun isEnabled(context: Context): Boolean = + context.sharedPreferences.getBoolean(context.getString(R.string.pref_enable_kugou), true) + override suspend fun getLyrics(id: String, title: String, artist: String, duration: Int): Result = KuGou.getLyrics(title, artist, duration) } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/lyrics/LyricsProvider.kt b/app/src/main/java/com/zionhuang/music/lyrics/LyricsProvider.kt index 585cc1617..71737a725 100644 --- a/app/src/main/java/com/zionhuang/music/lyrics/LyricsProvider.kt +++ b/app/src/main/java/com/zionhuang/music/lyrics/LyricsProvider.kt @@ -1,5 +1,8 @@ package com.zionhuang.music.lyrics +import android.content.Context + interface LyricsProvider { + fun isEnabled(context: Context): Boolean suspend fun getLyrics(id: String, title: String, artist: String, duration: Int): Result } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/lyrics/YouTubeLyricsProvider.kt b/app/src/main/java/com/zionhuang/music/lyrics/YouTubeLyricsProvider.kt index 6973e35fe..fb0315e6c 100644 --- a/app/src/main/java/com/zionhuang/music/lyrics/YouTubeLyricsProvider.kt +++ b/app/src/main/java/com/zionhuang/music/lyrics/YouTubeLyricsProvider.kt @@ -1,9 +1,11 @@ package com.zionhuang.music.lyrics +import android.content.Context import com.zionhuang.innertube.YouTube import com.zionhuang.innertube.models.WatchEndpoint object YouTubeLyricsProvider : LyricsProvider { + override fun isEnabled(context: Context) = true override suspend fun getLyrics(id: String, title: String, artist: String, duration: Int): Result = YouTube.next(WatchEndpoint(videoId = id)).mapCatching { nextResult -> YouTube.browse(nextResult.lyricsEndpoint ?: throw IllegalStateException("Lyrics endpoint not found")).getOrThrow() diff --git a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt index 73b4ee873..f6a1ee49b 100644 --- a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt +++ b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt @@ -303,11 +303,13 @@ class SongPlayer( private suspend fun loadLyrics(mediaMetadata: MediaMetadata) { lyricProviders.forEach { provider -> - provider.getLyrics(mediaMetadata.id, mediaMetadata.title, mediaMetadata.artists.joinToString { it.name }, mediaMetadata.duration).onSuccess { lyrics -> - localRepository.upsert(LyricsEntity(mediaMetadata.id, lyrics)) - return - }.onFailure { - it.printStackTrace() + if (provider.isEnabled(context)) { + provider.getLyrics(mediaMetadata.id, mediaMetadata.title, mediaMetadata.artists.joinToString { it.name }, mediaMetadata.duration).onSuccess { lyrics -> + localRepository.upsert(LyricsEntity(mediaMetadata.id, lyrics)) + return + }.onFailure { + it.printStackTrace() + } } } localRepository.upsert(LyricsEntity(mediaMetadata.id, LYRICS_NOT_FOUND)) diff --git a/app/src/main/res/values-es-rUS/strings.xml b/app/src/main/res/values-es-rUS/strings.xml index 1a702ff56..e0694c9cf 100644 --- a/app/src/main/res/values-es-rUS/strings.xml +++ b/app/src/main/res/values-es-rUS/strings.xml @@ -58,6 +58,7 @@ Pause search history Clear search history Are you sure to clear all search history? + Enable KuGou lyrics provider Backup and restore Backup diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 3ddea90dc..3f06d5dd5 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -58,6 +58,7 @@ Pause search history Clear search history Are you sure to clear all search history? + Enable KuGou lyrics provider Backup and restore Backup diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index ee394f590..2fe40a53b 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -58,6 +58,7 @@ متوقف‌کردن تاریخچه جستجو پاک‌کردن تاریخچه جستجو آیا برای پاک‌کردن تمام سابقه جستجو مطمئن هستید؟ + Enable KuGou lyrics provider پشتیبان‌گیری و بازگردانی پشتیبان‌گیری diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index c3d21c4e6..5c192fd5e 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -58,6 +58,7 @@ Pause search history Clear search history Are you sure to clear all search history? + Enable KuGou lyrics provider Backup and restore Backup diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 893b10307..2a77d09b6 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -58,6 +58,7 @@ Pause search history Clear search history Are you sure to clear all search history? + Enable KuGou lyrics provider Backup and restore Backup diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 3075a040d..6ea009674 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -58,6 +58,7 @@ Keresési előzmények szüneteltetése Keresési előzmények törlése Biztosan töröl minden keresési előzményt? + Enable KuGou lyrics provider Bizt.mentés és visszaállítás Bizt.mentés diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 6e5ebe5bb..c661fba2c 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -58,6 +58,7 @@ Sospendi la cronologia delle ricerche Pulisci la cronologia delle ricerche Sei sicuro di voler cancellare la cronologia delle ricerche? + Enable KuGou lyrics provider Backup e ripristina Backup diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 60917c1f6..2c8987ef0 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -58,6 +58,7 @@ 履歴の記録を一時停止 履歴を削除 すべての検索履歴を削除しますか? + Enable KuGou lyrics provider バックアップとリストア バックアップ diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index f478b52c4..c924314f8 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -58,6 +58,7 @@ Pause search history Clear search history Are you sure to clear all search history? + Enable KuGou lyrics provider Backup and restore Backup diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml index f656f1178..b0371955f 100644 --- a/app/src/main/res/values-ml-rIN/strings.xml +++ b/app/src/main/res/values-ml-rIN/strings.xml @@ -58,6 +58,7 @@ തിരയൽ ചരിത്രം താൽക്കാലികമായി നിർത്തുക തിരയൽ ചരിത്രം മായ്‌ക്കുക എല്ലാ തിരയൽ ചരിത്രവും മായ്‌ക്കണമെന്ന് ഉറപ്പാണോ? + Enable KuGou lyrics provider ബാക്കപ്പും വീണ്ടെടുക്കലും ബാക്കപ്പ് diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 6c3469c35..d43b6802c 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -58,6 +58,7 @@ Pausar histórico de pesquisa Limpar histórico de pesquisa Tem certeza que deseja deletar todo o seu histórico de pesquisa? + Enable KuGou lyrics provider Backup e restauração Backup diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index cfeef0c69..47d561aec 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -58,6 +58,7 @@ Pause search history Clear search history Are you sure to clear all search history? + Enable KuGou lyrics provider Backup and restore Backup diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 73bee1ab5..1124c7dd5 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -58,6 +58,7 @@ 暂停搜索记录 清除搜索记录 您确定要清除所有搜索记录吗? + Enable KuGou lyrics provider 备份与还原 备份 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 2f3147899..0e89ca5fc 100755 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -58,6 +58,7 @@ 暫停搜尋紀錄 清除搜尋紀錄 您確定要清除所有搜尋紀錄嗎? + 使用酷狗音樂提供歌詞 備份與還原 備份 diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index 385104b57..c03e53e21 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -47,6 +47,7 @@ PAUSE_SEARCH_HISTORY CLEAR_SEARCH_HISTORY + ENABLE_KUGOU BACKUP RESTORE diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3e2eefa15..dca263dd2 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -58,6 +58,7 @@ Pause search history Clear search history Are you sure to clear all search history? + Enable KuGou lyrics provider Backup and restore Backup diff --git a/app/src/main/res/xml/pref_privacy.xml b/app/src/main/res/xml/pref_privacy.xml index c4404d058..735242102 100644 --- a/app/src/main/res/xml/pref_privacy.xml +++ b/app/src/main/res/xml/pref_privacy.xml @@ -9,4 +9,9 @@ android:icon="@drawable/ic_clear_all" android:key="@string/pref_clear_search_history" android:title="@string/pref_clear_search_history_title" /> + \ No newline at end of file From c8c241e8ee510c9b6327df23a4bb5c93dda67678 Mon Sep 17 00:00:00 2001 From: gidano Date: Sun, 23 Oct 2022 09:11:52 +0200 Subject: [PATCH 22/95] Update hu strings Update the Hungarian language file --- app/src/main/res/values-hu/strings.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 6ea009674..50c99e7b3 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -20,10 +20,10 @@ Rendszer szerint Alapért. nyitott lap A navigációs lapok testreszabása - Lyrics text position - Left - Center - Right + Dalszöveg szöveg pozíció + Balra + Középre + Jobbra Tartalom A tartalom alapért. nyelve @@ -42,9 +42,9 @@ Equalizer Gyorsítótár - Max. kép gyors.tár + Max. kép gyorsítótár Kép gyors.tár törlés - Max. dal gyors.tár + Max. dal gyorsítótár %s használva Általános @@ -84,7 +84,7 @@ Sárga Amber Narancs - Mély narancs + Sötét narancs Barna Kékesszürke @@ -288,5 +288,5 @@ Keresett dalok - Lyrics not found + A dalszöveg nem található From ab2238ed7f4baa91ca3571a8d067f725fc6f12b5 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sun, 23 Oct 2022 17:30:45 +0800 Subject: [PATCH 23/95] Fix lyrics text color (#350) --- app/src/main/res/layout/bottom_controls_sheet.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/bottom_controls_sheet.xml b/app/src/main/res/layout/bottom_controls_sheet.xml index 4dbf15238..d3ec74a46 100644 --- a/app/src/main/res/layout/bottom_controls_sheet.xml +++ b/app/src/main/res/layout/bottom_controls_sheet.xml @@ -105,14 +105,14 @@ app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" app:lrcCurrentTextColor="?colorPrimary" app:lrcLabel="@string/lyrics_not_found" - app:lrcNormalTextColor="?android:textColorSecondaryInverse" + app:lrcNormalTextColor="@color/m3_dark_default_color_secondary_text" app:lrcNormalTextSize="20sp" app:lrcPadding="16dp" app:lrcTextGravity="center" app:lrcTextSize="24sp" app:lrcTimelineColor="#00000000" - app:lrcTimelineTextColor="?android:textColorSecondaryInverse" - app:lrcUnsyncedTextColor="?android:textColorPrimaryInverse" + app:lrcTimelineTextColor="@color/m3_dark_default_color_secondary_text" + app:lrcUnsyncedTextColor="@color/m3_dark_default_color_primary_text" tools:visibility="visible" /> Date: Sun, 23 Oct 2022 17:40:06 +0800 Subject: [PATCH 24/95] Tweak player view layout --- .../main/res/layout/bottom_controls_sheet.xml | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/layout/bottom_controls_sheet.xml b/app/src/main/res/layout/bottom_controls_sheet.xml index d3ec74a46..9be196341 100644 --- a/app/src/main/res/layout/bottom_controls_sheet.xml +++ b/app/src/main/res/layout/bottom_controls_sheet.xml @@ -113,6 +113,7 @@ app:lrcTimelineColor="#00000000" app:lrcTimelineTextColor="@color/m3_dark_default_color_secondary_text" app:lrcUnsyncedTextColor="@color/m3_dark_default_color_primary_text" + tools:ignore="PrivateResource" tools:visibility="visible" /> - @@ -209,18 +211,24 @@ android:layout_width="36dp" android:layout_height="36dp" android:layout_gravity="center_vertical" - android:layout_marginHorizontal="12dp" android:background="?selectableItemBackgroundBorderless" android:padding="4dp" - android:src="@drawable/ic_favorite" /> + android:src="@drawable/ic_favorite" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/btn_previous" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> @@ -229,11 +237,14 @@ android:layout_width="64dp" android:layout_height="64dp" android:layout_gravity="center_vertical" - android:layout_marginHorizontal="12dp" android:background="@drawable/btn_play_pause_background" android:onClick="@{()->viewModel.togglePlayPause()}" android:padding="16dp" android:tint="?colorOnPrimary" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/btn_next" + app:layout_constraintStart_toEndOf="@id/btn_previous" + app:layout_constraintTop_toTopOf="parent" app:playPauseButtonTint="?colorOnPrimary" app:playState="@{viewModel.playbackState.state}" tools:ignore="SpeakableTextPresentCheck" @@ -244,9 +255,12 @@ android:layout_width="36dp" android:layout_height="36dp" android:layout_gravity="center_vertical" - android:layout_marginHorizontal="12dp" android:onClick="@{()->viewModel.transportControls.skipToNext()}" app:enabled="@{(viewModel.playbackState.actions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0}" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/btn_repeat" + app:layout_constraintStart_toEndOf="@id/btn_play_pause" + app:layout_constraintTop_toTopOf="parent" tools:ignore="SpeakableTextPresentCheck" tools:src="@drawable/ic_skip_next" /> @@ -255,14 +269,17 @@ android:layout_width="36dp" android:layout_height="36dp" android:layout_gravity="center_vertical" - android:layout_marginHorizontal="12dp" android:clickable="true" android:focusable="true" android:onClick="@{()->viewModel.toggleRepeatMode()}" android:padding="4dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/btn_next" + app:layout_constraintTop_toTopOf="parent" app:repeatMode="@{viewModel.playbackState.repeatMode}" tools:ignore="SpeakableTextPresentCheck" tools:src="@drawable/ic_repeat" /> - + \ No newline at end of file From 3f9594fae2e3eb424b178b1dc60e63a5bef5cbca Mon Sep 17 00:00:00 2001 From: Sdarfeesh <50188628+Sdarfeesh@users.noreply.github.com> Date: Sun, 23 Oct 2022 20:27:05 +0800 Subject: [PATCH 25/95] Update zh-rCN --- app/src/main/res/values-zh-rCN/strings.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 1124c7dd5..b1fcc0551 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -20,10 +20,10 @@ 跟随系统 默认启动选项卡 自定义导航选项卡 - Lyrics text position - Left - Center - Right + 歌词文字位置 + 靠左 + 置中 + 靠右 内容 默认内容语言 @@ -58,7 +58,7 @@ 暂停搜索记录 清除搜索记录 您确定要清除所有搜索记录吗? - Enable KuGou lyrics provider + 使用酷狗音乐提供歌词 备份与还原 备份 @@ -282,5 +282,5 @@ 搜索的歌曲 - Lyrics not found + 无歌词 From 2d6e3f40497783bdd5f49e3bfa2bca510b607b96 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Mon, 24 Oct 2022 13:34:48 +0800 Subject: [PATCH 26/95] Fix image cache --- app/src/main/java/com/zionhuang/music/App.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/zionhuang/music/App.kt b/app/src/main/java/com/zionhuang/music/App.kt index bfdeac46d..6be104734 100644 --- a/app/src/main/java/com/zionhuang/music/App.kt +++ b/app/src/main/java/com/zionhuang/music/App.kt @@ -65,6 +65,7 @@ class App : Application(), ImageLoaderFactory { override fun newImageLoader() = ImageLoader.Builder(this) .crossfade(true) + .respectCacheHeaders(false) .diskCache( DiskCache.Builder() .directory(cacheDir.resolve("coil")) From c97463b2d36fc1249e0118b656309282e7737b92 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Mon, 24 Oct 2022 13:39:44 +0800 Subject: [PATCH 27/95] Fix #353 --- .../music/ui/fragments/BottomControlsFragment.kt | 1 - .../com/zionhuang/music/ui/widgets/LyricsView.kt | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt index 57360813a..c08783577 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt @@ -91,7 +91,6 @@ class BottomControlsFragment : Fragment() { } PreferenceLiveData(requireContext(), R.string.pref_lyrics_text_position, "1").observe(viewLifecycleOwner) { binding.lyricsView.setTextGravity(it.toIntOrNull() ?: 1) - binding.lyricsView.invalidate() } lifecycleScope.launch { viewModel.playbackState.collect { playbackState -> diff --git a/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt b/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt index 708a614ec..858296ee2 100644 --- a/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt +++ b/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt @@ -220,7 +220,9 @@ class LyricsView @JvmOverloads constructor( fun setTextGravity(gravity: Int) { textGravity = gravity - initEntryList() + lrcEntryList.forEach { lrcEntry -> + lrcEntry.init(lrcPaint, lrcWidth.toInt(), textGravity) + } postInvalidate() } @@ -228,9 +230,9 @@ class LyricsView @JvmOverloads constructor( runOnUi { reset() viewScope.launch(Dispatchers.IO) { - val entries = if (lyrics.startsWith("[")) { + val entries = if (lyrics.startsWith("[")) { // synced listOf(LyricsEntry(0L, "")) + LyricsUtils.parseLyrics(lyrics) - } else { + } else { // unsynced lyrics.lines().mapIndexed { index, line -> LyricsEntry(index * 100L, line) } } isSyncedLyrics = lyrics.startsWith("[") @@ -252,11 +254,9 @@ class LyricsView @JvmOverloads constructor( if (line != currentLine) { previousLine = currentLine currentLine = line + updateCurrentTextSize(animate) if (!isFling && !inActive) { smoothScrollTo(line, if (animate) animationDuration else 0) - updateCurrentTextSize(animate) - } else { - invalidate() } } } From 8e535e186cad5bb6dfe311c29def97b3b13e3f92 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Mon, 24 Oct 2022 13:43:24 +0800 Subject: [PATCH 28/95] Fix #353 --- app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt b/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt index 858296ee2..399119aa7 100644 --- a/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt +++ b/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt @@ -219,6 +219,9 @@ class LyricsView @JvmOverloads constructor( } fun setTextGravity(gravity: Int) { + if (textGravity == gravity) { + return + } textGravity = gravity lrcEntryList.forEach { lrcEntry -> lrcEntry.init(lrcPaint, lrcWidth.toInt(), textGravity) From 85f39b633c7910c700a4a6c9fd89bd05c7a0b5b5 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Mon, 24 Oct 2022 13:45:53 +0800 Subject: [PATCH 29/95] Support Android Auto browse (#125) --- .../zionhuang/music/playback/MusicService.kt | 130 ++++++++++++++++-- .../zionhuang/music/playback/SongPlayer.kt | 36 +++-- app/src/main/res/drawable/ic_music_note.xml | 5 +- 3 files changed, 135 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/playback/MusicService.kt b/app/src/main/java/com/zionhuang/music/playback/MusicService.kt index 46bc0b933..96a6a4de4 100644 --- a/app/src/main/java/com/zionhuang/music/playback/MusicService.kt +++ b/app/src/main/java/com/zionhuang/music/playback/MusicService.kt @@ -1,19 +1,38 @@ package com.zionhuang.music.playback import android.app.Notification +import android.content.ContentResolver import android.content.Intent +import android.net.Uri import android.os.Binder import android.os.Bundle import android.os.IBinder import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaBrowserCompat.MediaItem.FLAG_BROWSABLE +import android.support.v4.media.MediaBrowserCompat.MediaItem.FLAG_PLAYABLE import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.session.MediaSessionCompat +import android.util.Log +import android.widget.Toast +import android.widget.Toast.LENGTH_SHORT +import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat.startForegroundService +import androidx.core.net.toUri import androidx.lifecycle.lifecycleScope import androidx.media.session.MediaButtonReceiver import com.google.android.exoplayer2.ui.PlayerNotificationManager import com.google.android.exoplayer2.upstream.cache.SimpleCache +import com.zionhuang.music.R +import com.zionhuang.music.constants.Constants.DOWNLOADED_PLAYLIST_ID +import com.zionhuang.music.constants.Constants.LIKED_PLAYLIST_ID +import com.zionhuang.music.models.sortInfo.AlbumSortInfoPreference +import com.zionhuang.music.models.sortInfo.ArtistSortInfoPreference +import com.zionhuang.music.models.sortInfo.PlaylistSortInfoPreference +import com.zionhuang.music.models.sortInfo.SongSortInfoPreference +import com.zionhuang.music.models.toMediaMetadata +import com.zionhuang.music.repos.SongRepository +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking class MusicService : LifecycleMediaBrowserService() { private val binder = MusicBinder() @@ -61,6 +80,7 @@ class MusicService : LifecycleMediaBrowserService() { super.onTaskRemoved(rootIntent) stopSelf() } + inner class MusicBinder : Binder() { val sessionToken: MediaSessionCompat.Token get() = songPlayer.mediaSession.sessionToken @@ -72,21 +92,101 @@ class MusicService : LifecycleMediaBrowserService() { get() = this@MusicService.songPlayer.cache } - // TODO: Support Android Auto - private val ROOT_ID = "root" - - override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot = BrowserRoot(ROOT_ID, null) - - override fun onLoadChildren(parentId: String, result: Result>) { - if (parentId == ROOT_ID) { - result.sendResult(mutableListOf(MediaBrowserCompat.MediaItem( - MediaDescriptionCompat.Builder() - .setMediaId("id_all") - .setTitle("All") - .build(), - FLAG_BROWSABLE))) - } else { - result.sendResult(mutableListOf()) + + override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot = BrowserRoot(ROOT, null) + + override fun onLoadChildren(parentId: String, result: Result>) = runBlocking { + Toast.makeText(this@MusicService, "parentId: $parentId", LENGTH_SHORT).show() + Log.d("DBG", "parentId: $parentId") + when (parentId) { + ROOT -> result.sendResult(mutableListOf( + MediaBrowserItem(SONG, getString(R.string.title_songs), null, drawableUri(R.drawable.ic_music_note)), + MediaBrowserItem(ARTIST, getString(R.string.title_artists), null, drawableUri(R.drawable.ic_artist)), + MediaBrowserItem(ALBUM, getString(R.string.title_albums), null, drawableUri(R.drawable.ic_album)), + MediaBrowserItem(PLAYLIST, getString(R.string.title_playlists), null, drawableUri(R.drawable.ic_queue_music)) + )) + SONG -> { + result.detach() + result.sendResult(SongRepository.getAllSongs(SongSortInfoPreference).flow.first().map { + MediaBrowserCompat.MediaItem(it.toMediaMetadata().toMediaDescription(this@MusicService), FLAG_PLAYABLE) + }.toMutableList()) + } + ARTIST -> { + result.detach() + result.sendResult(SongRepository.getAllArtists(ArtistSortInfoPreference).flow.first().map { artist -> + MediaBrowserItem("$ARTIST/${artist.id}", artist.artist.name, resources.getQuantityString(R.plurals.song_count, artist.songCount, artist.songCount), artist.artist.thumbnailUrl?.toUri()) + }.toMutableList()) + } + ALBUM -> { + result.detach() + result.sendResult(SongRepository.getAllAlbums(AlbumSortInfoPreference).flow.first().map { album -> + MediaBrowserItem("$ALBUM/${album.id}", album.album.title, album.artists.joinToString(), album.album.thumbnailUrl?.toUri()) + }.toMutableList()) + } + PLAYLIST -> { + result.detach() + val likedSongCount = SongRepository.getLikedSongCount().first() + val downloadedSongCount = SongRepository.getDownloadedSongCount().first() + result.sendResult((listOf( + MediaBrowserItem("$PLAYLIST/$LIKED_PLAYLIST_ID", getString(R.string.liked_songs), resources.getQuantityString(R.plurals.song_count, likedSongCount, likedSongCount), drawableUri(R.drawable.ic_favorite)), + MediaBrowserItem("$PLAYLIST/$DOWNLOADED_PLAYLIST_ID", getString(R.string.downloaded_songs), resources.getQuantityString(R.plurals.song_count, downloadedSongCount, downloadedSongCount), drawableUri(R.drawable.ic_save_alt)) + ) + SongRepository.getAllPlaylists(PlaylistSortInfoPreference).flow.first().filter { it.playlist.isLocalPlaylist }.map { playlist -> + MediaBrowserItem(playlist.id, playlist.playlist.name, resources.getQuantityString(R.plurals.song_count, playlist.songCount, playlist.songCount), playlist.playlist.thumbnailUrl?.toUri() ?: playlist.thumbnails.firstOrNull()?.toUri()) + }).toMutableList()) + } + else -> when { + parentId.startsWith("$ARTIST/") -> { + result.detach() + result.sendResult(SongRepository.getArtistSongs(parentId.removePrefix("$ARTIST/"), SongSortInfoPreference).flow.first().map { + MediaBrowserCompat.MediaItem(it.toMediaMetadata().toMediaDescription(this@MusicService), FLAG_PLAYABLE) + }.toMutableList()) + } + parentId.startsWith("$ALBUM/") -> { + result.detach() + result.sendResult(SongRepository.getAlbumSongs(parentId.removePrefix("$ALBUM/")).map { + MediaBrowserCompat.MediaItem(it.toMediaMetadata().toMediaDescription(this@MusicService), FLAG_PLAYABLE) + }.toMutableList()) + } + parentId.startsWith("$PLAYLIST/") -> { + result.detach() + result.sendResult(when (val playlistId = parentId.removePrefix("$PLAYLIST/")) { + LIKED_PLAYLIST_ID -> SongRepository.getLikedSongs(SongSortInfoPreference) + DOWNLOADED_PLAYLIST_ID -> SongRepository.getDownloadedSongs(SongSortInfoPreference) + else -> SongRepository.getPlaylistSongs(playlistId) + }.flow.first().map { + MediaBrowserCompat.MediaItem(it.toMediaMetadata().toMediaDescription(this@MusicService), FLAG_PLAYABLE) + }.toMutableList()) + } + else -> { + result.sendResult(mutableListOf()) + } + } } } + + private fun drawableUri(@DrawableRes id: Int) = Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(resources.getResourcePackageName(id)) + .appendPath(resources.getResourceTypeName(id)) + .appendPath(resources.getResourceEntryName(id)) + .build() + + private fun MediaBrowserItem(id: String, title: String, subtitle: String?, iconUri: Uri?, flags: Int = FLAG_BROWSABLE) = + MediaBrowserCompat.MediaItem( + MediaDescriptionCompat.Builder() + .setMediaId(id) + .setTitle(title) + .setSubtitle(subtitle) + .setIconUri(iconUri) + .build(), + flags + ) + + companion object { + const val ROOT = "root" + const val SONG = "song" + const val ARTIST = "artist" + const val ALBUM = "album" + const val PLAYLIST = "playlist" + } } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt index f6a1ee49b..7a033eb7f 100644 --- a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt +++ b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt @@ -13,7 +13,7 @@ import android.os.Bundle import android.os.ResultReceiver import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.session.MediaSessionCompat -import android.support.v4.media.session.PlaybackStateCompat +import android.support.v4.media.session.PlaybackStateCompat.* import android.util.Pair import androidx.core.app.NotificationCompat import androidx.core.content.getSystemService @@ -23,6 +23,7 @@ import androidx.core.util.component2 import com.google.android.exoplayer2.* import com.google.android.exoplayer2.PlaybackException.* import com.google.android.exoplayer2.Player.* +import com.google.android.exoplayer2.Player.State import com.google.android.exoplayer2.analytics.AnalyticsListener import com.google.android.exoplayer2.analytics.PlaybackStats import com.google.android.exoplayer2.analytics.PlaybackStatsListener @@ -145,8 +146,13 @@ class SongPlayer( setPlayer(player) setPlaybackPreparer(object : MediaSessionConnector.PlaybackPreparer { override fun onCommand(player: Player, command: String, extras: Bundle?, cb: ResultReceiver?) = false - override fun getSupportedPrepareActions(): Long = 0L - override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {} + override fun getSupportedPrepareActions(): Long = ACTION_PREPARE or + ACTION_PREPARE_FROM_MEDIA_ID or + ACTION_PLAY_FROM_MEDIA_ID + + override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) { + } + override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {} override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) {} override fun onPrepare(playWhenReady: Boolean) { @@ -191,8 +197,8 @@ class SongPlayer( toggleLike() } - override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? = if (currentMediaMetadata.value != null) { - PlaybackStateCompat.CustomAction.Builder( + override fun getCustomAction(player: Player) = if (currentMediaMetadata.value != null) { + CustomAction.Builder( ACTION_TOGGLE_LIKE, context.getString(if (currentSong?.song?.liked == true) R.string.action_remove_like else R.string.action_like), if (currentSong?.song?.liked == true) R.drawable.ic_favorite else R.drawable.ic_favorite_border @@ -204,8 +210,8 @@ class SongPlayer( toggleLibrary() } - override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? = if (currentMediaMetadata.value != null) { - PlaybackStateCompat.CustomAction.Builder( + override fun getCustomAction(player: Player) = if (currentMediaMetadata.value != null) { + CustomAction.Builder( ACTION_TOGGLE_LIBRARY, context.getString(if (currentSong != null) R.string.action_remove_from_library else R.string.action_add_to_library), if (currentSong != null) R.drawable.ic_library_add_check else R.drawable.ic_library_add @@ -255,24 +261,16 @@ class SongPlayer( .setCustomActionReceiver(object : CustomActionReceiver { override fun createCustomActions(context: Context, instanceId: Int): Map = mapOf( ACTION_ADD_TO_LIBRARY to NotificationCompat.Action.Builder( - R.drawable.ic_library_add, - context.getString(R.string.action_add_to_library), - createPendingIntent(context, ACTION_ADD_TO_LIBRARY, instanceId) + R.drawable.ic_library_add, context.getString(R.string.action_add_to_library), createPendingIntent(context, ACTION_ADD_TO_LIBRARY, instanceId) ).build(), ACTION_REMOVE_FROM_LIBRARY to NotificationCompat.Action.Builder( - R.drawable.ic_library_add_check, - context.getString(R.string.action_remove_from_library), - createPendingIntent(context, ACTION_REMOVE_FROM_LIBRARY, instanceId) + R.drawable.ic_library_add_check, context.getString(R.string.action_remove_from_library), createPendingIntent(context, ACTION_REMOVE_FROM_LIBRARY, instanceId) ).build(), ACTION_LIKE to NotificationCompat.Action.Builder( - R.drawable.ic_favorite_border, - context.getString(R.string.action_like), - createPendingIntent(context, ACTION_LIKE, instanceId) + R.drawable.ic_favorite_border, context.getString(R.string.action_like), createPendingIntent(context, ACTION_LIKE, instanceId) ).build(), ACTION_UNLIKE to NotificationCompat.Action.Builder( - R.drawable.ic_favorite, - context.getString(R.string.action_remove_like), - createPendingIntent(context, ACTION_UNLIKE, instanceId) + R.drawable.ic_favorite, context.getString(R.string.action_remove_like), createPendingIntent(context, ACTION_UNLIKE, instanceId) ).build() ) diff --git a/app/src/main/res/drawable/ic_music_note.xml b/app/src/main/res/drawable/ic_music_note.xml index efa98319d..13c018e0f 100644 --- a/app/src/main/res/drawable/ic_music_note.xml +++ b/app/src/main/res/drawable/ic_music_note.xml @@ -1,10 +1,11 @@ + From 36d091ff050a0e168e6217f9c5dc010a5d097122 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Mon, 24 Oct 2022 13:46:41 +0800 Subject: [PATCH 30/95] Remove debug toast --- .../zionhuang/music/playback/MusicService.kt | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/playback/MusicService.kt b/app/src/main/java/com/zionhuang/music/playback/MusicService.kt index 96a6a4de4..9a48242c0 100644 --- a/app/src/main/java/com/zionhuang/music/playback/MusicService.kt +++ b/app/src/main/java/com/zionhuang/music/playback/MusicService.kt @@ -12,9 +12,6 @@ import android.support.v4.media.MediaBrowserCompat.MediaItem.FLAG_BROWSABLE import android.support.v4.media.MediaBrowserCompat.MediaItem.FLAG_PLAYABLE import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.session.MediaSessionCompat -import android.util.Log -import android.widget.Toast -import android.widget.Toast.LENGTH_SHORT import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat.startForegroundService import androidx.core.net.toUri @@ -96,14 +93,12 @@ class MusicService : LifecycleMediaBrowserService() { override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot = BrowserRoot(ROOT, null) override fun onLoadChildren(parentId: String, result: Result>) = runBlocking { - Toast.makeText(this@MusicService, "parentId: $parentId", LENGTH_SHORT).show() - Log.d("DBG", "parentId: $parentId") when (parentId) { ROOT -> result.sendResult(mutableListOf( - MediaBrowserItem(SONG, getString(R.string.title_songs), null, drawableUri(R.drawable.ic_music_note)), - MediaBrowserItem(ARTIST, getString(R.string.title_artists), null, drawableUri(R.drawable.ic_artist)), - MediaBrowserItem(ALBUM, getString(R.string.title_albums), null, drawableUri(R.drawable.ic_album)), - MediaBrowserItem(PLAYLIST, getString(R.string.title_playlists), null, drawableUri(R.drawable.ic_queue_music)) + mediaBrowserItem(SONG, getString(R.string.title_songs), null, drawableUri(R.drawable.ic_music_note)), + mediaBrowserItem(ARTIST, getString(R.string.title_artists), null, drawableUri(R.drawable.ic_artist)), + mediaBrowserItem(ALBUM, getString(R.string.title_albums), null, drawableUri(R.drawable.ic_album)), + mediaBrowserItem(PLAYLIST, getString(R.string.title_playlists), null, drawableUri(R.drawable.ic_queue_music)) )) SONG -> { result.detach() @@ -114,13 +109,13 @@ class MusicService : LifecycleMediaBrowserService() { ARTIST -> { result.detach() result.sendResult(SongRepository.getAllArtists(ArtistSortInfoPreference).flow.first().map { artist -> - MediaBrowserItem("$ARTIST/${artist.id}", artist.artist.name, resources.getQuantityString(R.plurals.song_count, artist.songCount, artist.songCount), artist.artist.thumbnailUrl?.toUri()) + mediaBrowserItem("$ARTIST/${artist.id}", artist.artist.name, resources.getQuantityString(R.plurals.song_count, artist.songCount, artist.songCount), artist.artist.thumbnailUrl?.toUri()) }.toMutableList()) } ALBUM -> { result.detach() result.sendResult(SongRepository.getAllAlbums(AlbumSortInfoPreference).flow.first().map { album -> - MediaBrowserItem("$ALBUM/${album.id}", album.album.title, album.artists.joinToString(), album.album.thumbnailUrl?.toUri()) + mediaBrowserItem("$ALBUM/${album.id}", album.album.title, album.artists.joinToString(), album.album.thumbnailUrl?.toUri()) }.toMutableList()) } PLAYLIST -> { @@ -128,10 +123,10 @@ class MusicService : LifecycleMediaBrowserService() { val likedSongCount = SongRepository.getLikedSongCount().first() val downloadedSongCount = SongRepository.getDownloadedSongCount().first() result.sendResult((listOf( - MediaBrowserItem("$PLAYLIST/$LIKED_PLAYLIST_ID", getString(R.string.liked_songs), resources.getQuantityString(R.plurals.song_count, likedSongCount, likedSongCount), drawableUri(R.drawable.ic_favorite)), - MediaBrowserItem("$PLAYLIST/$DOWNLOADED_PLAYLIST_ID", getString(R.string.downloaded_songs), resources.getQuantityString(R.plurals.song_count, downloadedSongCount, downloadedSongCount), drawableUri(R.drawable.ic_save_alt)) + mediaBrowserItem("$PLAYLIST/$LIKED_PLAYLIST_ID", getString(R.string.liked_songs), resources.getQuantityString(R.plurals.song_count, likedSongCount, likedSongCount), drawableUri(R.drawable.ic_favorite)), + mediaBrowserItem("$PLAYLIST/$DOWNLOADED_PLAYLIST_ID", getString(R.string.downloaded_songs), resources.getQuantityString(R.plurals.song_count, downloadedSongCount, downloadedSongCount), drawableUri(R.drawable.ic_save_alt)) ) + SongRepository.getAllPlaylists(PlaylistSortInfoPreference).flow.first().filter { it.playlist.isLocalPlaylist }.map { playlist -> - MediaBrowserItem(playlist.id, playlist.playlist.name, resources.getQuantityString(R.plurals.song_count, playlist.songCount, playlist.songCount), playlist.playlist.thumbnailUrl?.toUri() ?: playlist.thumbnails.firstOrNull()?.toUri()) + mediaBrowserItem(playlist.id, playlist.playlist.name, resources.getQuantityString(R.plurals.song_count, playlist.songCount, playlist.songCount), playlist.playlist.thumbnailUrl?.toUri() ?: playlist.thumbnails.firstOrNull()?.toUri()) }).toMutableList()) } else -> when { @@ -171,7 +166,7 @@ class MusicService : LifecycleMediaBrowserService() { .appendPath(resources.getResourceEntryName(id)) .build() - private fun MediaBrowserItem(id: String, title: String, subtitle: String?, iconUri: Uri?, flags: Int = FLAG_BROWSABLE) = + private fun mediaBrowserItem(id: String, title: String, subtitle: String?, iconUri: Uri?, flags: Int = FLAG_BROWSABLE) = MediaBrowserCompat.MediaItem( MediaDescriptionCompat.Builder() .setMediaId(id) From 0b21864c6087ea3a9bad63dcbf206953f4f4780a Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 25 Oct 2022 17:46:15 +0800 Subject: [PATCH 31/95] Support Android Auto (#125) --- .../com/zionhuang/music/db/daos/AlbumDao.kt | 3 + .../zionhuang/music/playback/MusicService.kt | 10 +-- .../zionhuang/music/playback/SongPlayer.kt | 61 +++++++++++++++++++ .../zionhuang/music/repos/SongRepository.kt | 4 ++ .../music/repos/base/LocalRepository.kt | 1 + 5 files changed, 74 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/db/daos/AlbumDao.kt b/app/src/main/java/com/zionhuang/music/db/daos/AlbumDao.kt index 793db820a..99ae49cc3 100644 --- a/app/src/main/java/com/zionhuang/music/db/daos/AlbumDao.kt +++ b/app/src/main/java/com/zionhuang/music/db/daos/AlbumDao.kt @@ -30,6 +30,9 @@ interface AlbumDao { @Query("SELECT * FROM album WHERE title LIKE '%' || :query || '%' LIMIT :previewSize") fun searchAlbumsPreview(query: String, previewSize: Int): Flow> + @Query("SELECT * FROM album WHERE id = :id") + suspend fun getAlbumById(id: String): AlbumEntity? + @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insert(album: AlbumEntity): Long diff --git a/app/src/main/java/com/zionhuang/music/playback/MusicService.kt b/app/src/main/java/com/zionhuang/music/playback/MusicService.kt index 9a48242c0..07810f2f1 100644 --- a/app/src/main/java/com/zionhuang/music/playback/MusicService.kt +++ b/app/src/main/java/com/zionhuang/music/playback/MusicService.kt @@ -103,7 +103,7 @@ class MusicService : LifecycleMediaBrowserService() { SONG -> { result.detach() result.sendResult(SongRepository.getAllSongs(SongSortInfoPreference).flow.first().map { - MediaBrowserCompat.MediaItem(it.toMediaMetadata().toMediaDescription(this@MusicService), FLAG_PLAYABLE) + MediaBrowserCompat.MediaItem(it.toMediaMetadata().copy(id = "$parentId/${it.id}").toMediaDescription(this@MusicService), FLAG_PLAYABLE) }.toMutableList()) } ARTIST -> { @@ -126,20 +126,20 @@ class MusicService : LifecycleMediaBrowserService() { mediaBrowserItem("$PLAYLIST/$LIKED_PLAYLIST_ID", getString(R.string.liked_songs), resources.getQuantityString(R.plurals.song_count, likedSongCount, likedSongCount), drawableUri(R.drawable.ic_favorite)), mediaBrowserItem("$PLAYLIST/$DOWNLOADED_PLAYLIST_ID", getString(R.string.downloaded_songs), resources.getQuantityString(R.plurals.song_count, downloadedSongCount, downloadedSongCount), drawableUri(R.drawable.ic_save_alt)) ) + SongRepository.getAllPlaylists(PlaylistSortInfoPreference).flow.first().filter { it.playlist.isLocalPlaylist }.map { playlist -> - mediaBrowserItem(playlist.id, playlist.playlist.name, resources.getQuantityString(R.plurals.song_count, playlist.songCount, playlist.songCount), playlist.playlist.thumbnailUrl?.toUri() ?: playlist.thumbnails.firstOrNull()?.toUri()) + mediaBrowserItem("$PLAYLIST/${playlist.id}", playlist.playlist.name, resources.getQuantityString(R.plurals.song_count, playlist.songCount, playlist.songCount), playlist.playlist.thumbnailUrl?.toUri() ?: playlist.thumbnails.firstOrNull()?.toUri()) }).toMutableList()) } else -> when { parentId.startsWith("$ARTIST/") -> { result.detach() result.sendResult(SongRepository.getArtistSongs(parentId.removePrefix("$ARTIST/"), SongSortInfoPreference).flow.first().map { - MediaBrowserCompat.MediaItem(it.toMediaMetadata().toMediaDescription(this@MusicService), FLAG_PLAYABLE) + MediaBrowserCompat.MediaItem(it.toMediaMetadata().copy(id = "$parentId/${it.id}").toMediaDescription(this@MusicService), FLAG_PLAYABLE) }.toMutableList()) } parentId.startsWith("$ALBUM/") -> { result.detach() result.sendResult(SongRepository.getAlbumSongs(parentId.removePrefix("$ALBUM/")).map { - MediaBrowserCompat.MediaItem(it.toMediaMetadata().toMediaDescription(this@MusicService), FLAG_PLAYABLE) + MediaBrowserCompat.MediaItem(it.toMediaMetadata().copy(id = "$parentId/${it.id}").toMediaDescription(this@MusicService), FLAG_PLAYABLE) }.toMutableList()) } parentId.startsWith("$PLAYLIST/") -> { @@ -149,7 +149,7 @@ class MusicService : LifecycleMediaBrowserService() { DOWNLOADED_PLAYLIST_ID -> SongRepository.getDownloadedSongs(SongSortInfoPreference) else -> SongRepository.getPlaylistSongs(playlistId) }.flow.first().map { - MediaBrowserCompat.MediaItem(it.toMediaMetadata().toMediaDescription(this@MusicService), FLAG_PLAYABLE) + MediaBrowserCompat.MediaItem(it.toMediaMetadata().copy(id = "$parentId/${it.id}").toMediaDescription(this@MusicService), FLAG_PLAYABLE) }.toMutableList()) } else -> { diff --git a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt index 7a033eb7f..3cec848d9 100644 --- a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt +++ b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt @@ -49,6 +49,8 @@ import com.zionhuang.innertube.models.QueueAddEndpoint.Companion.INSERT_AFTER_CU import com.zionhuang.innertube.models.QueueAddEndpoint.Companion.INSERT_AT_END import com.zionhuang.music.R import com.zionhuang.music.constants.Constants.ACTION_SHOW_BOTTOM_SHEET +import com.zionhuang.music.constants.Constants.DOWNLOADED_PLAYLIST_ID +import com.zionhuang.music.constants.Constants.LIKED_PLAYLIST_ID import com.zionhuang.music.constants.MediaConstants.EXTRA_MEDIA_METADATA_ITEMS import com.zionhuang.music.constants.MediaConstants.STATE_DOWNLOADED import com.zionhuang.music.constants.MediaSessionConstants.ACTION_ADD_TO_LIBRARY @@ -70,6 +72,11 @@ import com.zionhuang.music.lyrics.KuGouLyricsProvider import com.zionhuang.music.lyrics.LyricsProvider import com.zionhuang.music.lyrics.YouTubeLyricsProvider import com.zionhuang.music.models.MediaMetadata +import com.zionhuang.music.models.sortInfo.SongSortInfoPreference +import com.zionhuang.music.playback.MusicService.Companion.ALBUM +import com.zionhuang.music.playback.MusicService.Companion.ARTIST +import com.zionhuang.music.playback.MusicService.Companion.PLAYLIST +import com.zionhuang.music.playback.MusicService.Companion.SONG import com.zionhuang.music.playback.queues.EmptyQueue import com.zionhuang.music.playback.queues.ListQueue import com.zionhuang.music.playback.queues.Queue @@ -151,6 +158,60 @@ class SongPlayer( ACTION_PLAY_FROM_MEDIA_ID override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) { + scope.launch { + val path = mediaId.split("/") + when (path.firstOrNull()) { + SONG -> { + val songId = path.getOrNull(1) ?: return@launch + val allSongs = SongRepository.getAllSongs(SongSortInfoPreference).flow.first() + playQueue(ListQueue( + title = context.getString(R.string.queue_all_songs), + items = allSongs.map { it.toMediaItem() }, + startIndex = allSongs.indexOfFirst { it.id == songId }.takeIf { it != -1 } ?: 0 + ), playWhenReady) + } + ARTIST -> { + val songId = path.getOrNull(2) ?: return@launch + val artistId = path.getOrNull(1) ?: return@launch + val artist = SongRepository.getArtistById(artistId) ?: return@launch + val songs = SongRepository.getArtistSongs(artistId, SongSortInfoPreference).flow.first() + playQueue(ListQueue( + title = artist.name, + items = songs.map { it.toMediaItem() }, + startIndex = songs.indexOfFirst { it.id == songId }.takeIf { it != -1 } ?: 0 + ), playWhenReady) + } + ALBUM -> { + val songId = path.getOrNull(2) ?: return@launch + val albumId = path.getOrNull(1) ?: return@launch + val album = SongRepository.getAlbum(albumId) ?: return@launch + val songs = SongRepository.getAlbumSongs(albumId) + playQueue(ListQueue( + title = album.title, + items = songs.map { it.toMediaItem() }, + startIndex = songs.indexOfFirst { it.id == songId }.takeIf { it != -1 } ?: 0 + ), playWhenReady) + } + PLAYLIST -> { + val songId = path.getOrNull(2) ?: return@launch + val playlistId = path.getOrNull(1) ?: return@launch + val songs = when (playlistId) { + LIKED_PLAYLIST_ID -> SongRepository.getLikedSongs(SongSortInfoPreference).flow.first() + DOWNLOADED_PLAYLIST_ID -> SongRepository.getDownloadedSongs(SongSortInfoPreference).flow.first() + else -> SongRepository.getPlaylistSongs(playlistId).getList() + } + playQueue(ListQueue( + title = when (playlistId) { + LIKED_PLAYLIST_ID -> context.getString(R.string.liked_songs) + DOWNLOADED_PLAYLIST_ID -> context.getString(R.string.downloaded_songs) + else -> SongRepository.getPlaylistById(playlistId).playlist.name + }, + items = songs.map { it.toMediaItem() }, + startIndex = songs.indexOfFirst { it.id == songId }.takeIf { it != -1 } ?: 0 + ), playWhenReady) + } + } + } } override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {} diff --git a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt index d9aace130..8abd08a79 100644 --- a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt +++ b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt @@ -532,6 +532,10 @@ object SongRepository : LocalRepository { } } + override suspend fun getAlbum(albumId: String) = withContext(IO) { + albumDao.getAlbumById(albumId) + } + override suspend fun deleteAlbums(albums: List) = withContext(IO) { albums.forEach { album -> val songs = songDao.getAlbumSongs(album.id).map { it.copy(album = null) } diff --git a/app/src/main/java/com/zionhuang/music/repos/base/LocalRepository.kt b/app/src/main/java/com/zionhuang/music/repos/base/LocalRepository.kt index d4f3d254b..1a6c25615 100644 --- a/app/src/main/java/com/zionhuang/music/repos/base/LocalRepository.kt +++ b/app/src/main/java/com/zionhuang/music/repos/base/LocalRepository.kt @@ -85,6 +85,7 @@ interface LocalRepository { suspend fun addAlbums(albums: List) suspend fun refetchAlbum(album: AlbumEntity) = refetchAlbums(listOf(album)) suspend fun refetchAlbums(albums: List) + suspend fun getAlbum(albumId: String): AlbumEntity? suspend fun deleteAlbums(albums: List) /** From 7a16e919eff8f9b86281b26afd7ce36bc4936df0 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 25 Oct 2022 18:09:58 +0800 Subject: [PATCH 32/95] [Feature] Skip silence --- .../zionhuang/music/playback/SongPlayer.kt | 26 ++++++++----------- app/src/main/res/values-es-rUS/strings.xml | 3 ++- app/src/main/res/values-es/strings.xml | 3 ++- app/src/main/res/values-fa-rIR/strings.xml | 3 ++- app/src/main/res/values-fi-rFI/strings.xml | 3 ++- app/src/main/res/values-fr-rFR/strings.xml | 3 ++- app/src/main/res/values-hu/strings.xml | 3 ++- app/src/main/res/values-it/strings.xml | 3 ++- app/src/main/res/values-ja-rJP/strings.xml | 3 ++- app/src/main/res/values-ko-rKR/strings.xml | 3 ++- app/src/main/res/values-ml-rIN/strings.xml | 3 ++- app/src/main/res/values-pt-rBR/strings.xml | 3 ++- app/src/main/res/values-sv-rSE/strings.xml | 3 ++- app/src/main/res/values-zh-rCN/strings.xml | 3 ++- app/src/main/res/values-zh-rTW/strings.xml | 3 ++- app/src/main/res/values/constants.xml | 1 + app/src/main/res/values/strings.xml | 3 ++- app/src/main/res/xml/pref_player_audio.xml | 14 ++++++---- 18 files changed, 51 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt index 3cec848d9..00547871c 100644 --- a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt +++ b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt @@ -153,10 +153,7 @@ class SongPlayer( setPlayer(player) setPlaybackPreparer(object : MediaSessionConnector.PlaybackPreparer { override fun onCommand(player: Player, command: String, extras: Bundle?, cb: ResultReceiver?) = false - override fun getSupportedPrepareActions(): Long = ACTION_PREPARE or - ACTION_PREPARE_FROM_MEDIA_ID or - ACTION_PLAY_FROM_MEDIA_ID - + override fun getSupportedPrepareActions(): Long = ACTION_PREPARE or ACTION_PREPARE_FROM_MEDIA_ID or ACTION_PLAY_FROM_MEDIA_ID override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) { scope.launch { val path = mediaId.split("/") @@ -254,10 +251,7 @@ class SongPlayer( } setCustomActionProviders( object : MediaSessionConnector.CustomActionProvider { - override fun onCustomAction(player: Player, action: String, extras: Bundle?) { - toggleLike() - } - + override fun onCustomAction(player: Player, action: String, extras: Bundle?) = toggleLike() override fun getCustomAction(player: Player) = if (currentMediaMetadata.value != null) { CustomAction.Builder( ACTION_TOGGLE_LIKE, @@ -267,10 +261,7 @@ class SongPlayer( } else null }, object : MediaSessionConnector.CustomActionProvider { - override fun onCustomAction(player: Player, action: String, extras: Bundle?) { - toggleLibrary() - } - + override fun onCustomAction(player: Player, action: String, extras: Bundle?) = toggleLibrary() override fun getCustomAction(player: Player) = if (currentMediaMetadata.value != null) { CustomAction.Builder( ACTION_TOGGLE_LIBRARY, @@ -376,9 +367,9 @@ class SongPlayer( init { scope.launch { - currentSongFlow.collect { - val shouldInvalidate = currentSong == null || it == null || currentSong?.song?.liked != it.song.liked - currentSong = it + currentSongFlow.collect { song -> + val shouldInvalidate = currentSong == null || song == null || currentSong?.song?.liked != song.song.liked + currentSong = song if (shouldInvalidate) { mediaSessionConnector.invalidateMediaSessionPlaybackState() playerNotificationManager.invalidate() @@ -395,6 +386,11 @@ class SongPlayer( } } } + scope.launch { + context.sharedPreferences.booleanFlow(context.getString(R.string.pref_skip_silence), true).collectLatest { + player.skipSilenceEnabled = it + } + } if (context.sharedPreferences.getBoolean(context.getString(R.string.pref_persistent_queue), true)) { runCatching { context.filesDir.resolve(PERSISTENT_QUEUE_FILE).inputStream().use { fis -> diff --git a/app/src/main/res/values-es-rUS/strings.xml b/app/src/main/res/values-es-rUS/strings.xml index e0694c9cf..980abf604 100644 --- a/app/src/main/res/values-es-rUS/strings.xml +++ b/app/src/main/res/values-es-rUS/strings.xml @@ -34,11 +34,12 @@ Restart to take effect Player and audio - Persistent queue Audio quality Auto High Low + Persistent queue + Skip silence Equalizer Cache diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 3f06d5dd5..78d6f2075 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -34,11 +34,12 @@ Restart to take effect Player and audio - Persistent queue Audio quality Auto High Low + Persistent queue + Skip silence Equalizer Cache diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index 2fe40a53b..5ad868490 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -34,11 +34,12 @@ راه‌اندازی‌مجدد برای اعمال اثر پخش‌کننده و صدا - Persistent queue کیفیت صدا خودکار بالا پایین + Persistent queue + Skip silence اکولایزر Cache diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 5c192fd5e..62c2e7d37 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -34,11 +34,12 @@ Restart to take effect Player and audio - Persistent queue Audio quality Auto High Low + Persistent queue + Skip silence Equalizer Cache diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 2a77d09b6..ad2a1ecc7 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -34,11 +34,12 @@ Restart to take effect Player and audio - Persistent queue Audio quality Auto High Low + Persistent queue + Skip silence Equalizer Cache diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 50c99e7b3..8cf3a7510 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -34,11 +34,12 @@ Indítsa újra, hogy életbe lépjen Lejátszó és hang - Állandó várósor Hangminőség Auto Magas Gyenge + Állandó várósor + Skip silence Equalizer Gyorsítótár diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index c661fba2c..44ee6407e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -34,11 +34,12 @@ Riavvia l\'app per applicare le modifiche Riproduttore e audio - Coda persistente Qualità dell\'audio Automatica Alta Bassa + Coda persistente + Skip silence Equalizzatore Cache diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 2c8987ef0..9e92562d9 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -34,11 +34,12 @@ 再起動して反映 プレイヤーとオーディオ - Persistent queue オーディオ品質 自動 + Persistent queue + Skip silence イコライザー Cache diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index c924314f8..06bfe4cbc 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -34,11 +34,12 @@ Restart to take effect Player and audio - Persistent queue Audio quality Auto High Low + Persistent queue + Skip silence Equalizer Cache diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml index b0371955f..d35024b76 100644 --- a/app/src/main/res/values-ml-rIN/strings.xml +++ b/app/src/main/res/values-ml-rIN/strings.xml @@ -34,11 +34,12 @@ പ്രാബല്യത്തിൽ വരാൻ പുനരാരംഭിക്കുക പ്ലെയറും ഓഡിയോയും - Persistent queue ഓഡിയോ നിലവാരം ഓട്ടോ ഉയർന്ന താഴ്ന്ന + Persistent queue + Skip silence ഇക്വലൈസർ Cache diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index d43b6802c..011aba5f1 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -34,11 +34,12 @@ Reiniciar para ativar proxy Player e áudio - Fila persistente Qualidade do áudio Automática Alta Baixa + Fila persistente + Skip silence Equalizador Cache diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 47d561aec..4000951fe 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -34,11 +34,12 @@ Restart to take effect Player and audio - Persistent queue Audio quality Auto High Low + Persistent queue + Skip silence Equalizer Cache diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b1fcc0551..b7169e675 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -34,11 +34,12 @@ 重启以应用变更 播放器与音频 - 保留播放队列 音质 自动 + 保留播放队列 + Skip silence 均衡器 缓存 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 0e89ca5fc..b1f186e53 100755 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -34,11 +34,12 @@ 重啟以套用變更 播放與音訊 - 保留播放佇列 音質 自動 + 保留播放佇列 + 跳過歌曲頭尾無聲片段 等化器 快取 diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index c03e53e21..abe051335 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -66,6 +66,7 @@ PLAYLIST_SORT_TYPE PLAYLIST_SORT_DESC SHOW_LYRICS + SKIP_SILENCE diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dca263dd2..c11c77be1 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -34,11 +34,12 @@ Restart to take effect Player and audio - Persistent queue Audio quality Auto High Low + Persistent queue + Skip silence Equalizer Cache diff --git a/app/src/main/res/xml/pref_player_audio.xml b/app/src/main/res/xml/pref_player_audio.xml index 435debd46..6ce2d52ef 100644 --- a/app/src/main/res/xml/pref_player_audio.xml +++ b/app/src/main/res/xml/pref_player_audio.xml @@ -1,10 +1,5 @@ - + + Date: Tue, 25 Oct 2022 22:45:12 +0800 Subject: [PATCH 33/95] [Feature] Audio normalization (#24) --- .../zionhuang/music/playback/SongPlayer.kt | 26 ++++++++++++++++--- .../ui/fragments/dialogs/SongDetailsDialog.kt | 7 +++++ .../music/viewmodels/LocalSearchViewModel.kt | 5 ++-- .../music/viewmodels/PlaybackViewModel.kt | 3 +++ .../main/res/layout/dialog_song_details.xml | 18 +++++++++++++ app/src/main/res/values-es-rUS/strings.xml | 2 ++ app/src/main/res/values-es/strings.xml | 2 ++ app/src/main/res/values-fa-rIR/strings.xml | 2 ++ app/src/main/res/values-fi-rFI/strings.xml | 2 ++ app/src/main/res/values-fr-rFR/strings.xml | 2 ++ app/src/main/res/values-hu/strings.xml | 2 ++ app/src/main/res/values-it/strings.xml | 2 ++ app/src/main/res/values-ja-rJP/strings.xml | 2 ++ app/src/main/res/values-ko-rKR/strings.xml | 2 ++ app/src/main/res/values-ml-rIN/strings.xml | 2 ++ app/src/main/res/values-pt-rBR/strings.xml | 2 ++ app/src/main/res/values-sv-rSE/strings.xml | 2 ++ app/src/main/res/values-zh-rCN/strings.xml | 2 ++ app/src/main/res/values-zh-rTW/strings.xml | 4 ++- app/src/main/res/values/constants.xml | 1 + app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/xml/pref_player_audio.xml | 4 +++ 22 files changed, 89 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt index 00547871c..ab12b7321 100644 --- a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt +++ b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt @@ -96,6 +96,8 @@ import java.io.ObjectOutputStream import java.net.ConnectException import java.net.SocketTimeoutException import java.net.UnknownHostException +import kotlin.math.min +import kotlin.math.pow import kotlin.math.roundToInt /** @@ -116,9 +118,13 @@ class SongPlayer( private var currentQueue: Queue = EmptyQueue() + val playerVolume = MutableStateFlow(1f) val currentMediaMetadata = MutableStateFlow(null) private val currentSongFlow = currentMediaMetadata.flatMapLatest { mediaMetadata -> - localRepository.getSongById(mediaMetadata?.id).getFlow() + localRepository.getSongById(mediaMetadata?.id).flow + } + private val currentFormat = currentMediaMetadata.flatMapLatest { mediaMetadata -> + localRepository.getSongFormat(mediaMetadata?.id).flow } var currentSong: Song? = null @@ -379,8 +385,7 @@ class SongPlayer( scope.launch { combine(currentMediaMetadata.distinctUntilChangedBy { it?.id }, showLyrics) { mediaMetadata, showLyrics -> Pair(mediaMetadata, showLyrics) - }.collectLatest { pair -> - val (mediaMetadata, showLyrics) = pair + }.collectLatest { (mediaMetadata, showLyrics) -> if (showLyrics && mediaMetadata != null && !localRepository.hasLyrics(mediaMetadata.id)) { loadLyrics(mediaMetadata) } @@ -391,6 +396,17 @@ class SongPlayer( player.skipSilenceEnabled = it } } + scope.launch { + combine(currentFormat, context.sharedPreferences.booleanFlow(context.getString(R.string.pref_audio_normalization), true)) { format, normalizeAudio -> + format to normalizeAudio + }.collectLatest { (format, normalizeAudio) -> + player.volume = if (normalizeAudio && format?.loudnessDb != null) { + min(10f.pow(-format.loudnessDb.toFloat() / 20), 1f) + } else { + 1f + } + } + } if (context.sharedPreferences.getBoolean(context.getString(R.string.pref_persistent_queue), true)) { runCatching { context.filesDir.resolve(PERSISTENT_QUEUE_FILE).inputStream().use { fis -> @@ -660,6 +676,10 @@ class SongPlayer( } } + override fun onVolumeChanged(volume: Float) { + playerVolume.value = volume + } + override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { if (shuffleModeEnabled) { // Always put current playing item at first diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/SongDetailsDialog.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/SongDetailsDialog.kt index 0eea1d3c3..4136516c9 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/SongDetailsDialog.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/SongDetailsDialog.kt @@ -1,5 +1,6 @@ package com.zionhuang.music.ui.fragments.dialogs +import android.annotation.SuppressLint import android.app.Dialog import android.content.ClipData import android.content.ClipboardManager @@ -24,6 +25,7 @@ class SongDetailsDialog : AppCompatDialogFragment() { private lateinit var binding: DialogSongDetailsBinding private val viewModel by activityViewModels() + @SuppressLint("SetTextI18n") private fun setupUI() { listOf(binding.songTitle, binding.songArtist, binding.mediaId, binding.mimeType, binding.codecs, binding.bitrate, binding.sampleRate, binding.loudness, binding.fileSize).forEach { textView -> textView.setOnClickListener { @@ -50,6 +52,11 @@ class SongDetailsDialog : AppCompatDialogFragment() { binding.fileSize.text = format?.contentLength?.let { Formatter.formatShortFileSize(requireContext(), it) } } } + lifecycleScope.launch { + viewModel.playerVolume.collectLatest { volume -> + binding.volume.text = "${(volume * 100).toInt()}%" + } + } } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/LocalSearchViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/LocalSearchViewModel.kt index 948bfcf67..1f75af17d 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/LocalSearchViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/LocalSearchViewModel.kt @@ -17,9 +17,8 @@ class LocalSearchViewModel(application: Application) : AndroidViewModel(applicat val query = SafeMutableLiveData("") val filter = SafeMutableLiveData(Filter.ALL) val result: Flow> = query.asFlow().combine(filter.asFlow()) { query: String, filter: Filter -> - Pair(query, filter) - }.flatMapLatest { - val (query, filter) = it + query to filter + }.flatMapLatest { (query, filter) -> if (query.isEmpty()) { emptyFlow() } else { diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/PlaybackViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/PlaybackViewModel.kt index 2bb515552..aa7f07d26 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/PlaybackViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/PlaybackViewModel.kt @@ -34,6 +34,9 @@ class PlaybackViewModel(application: Application) : AndroidViewModel(application val queueTitle = MediaSessionConnection.queueTitle val queueItems = MediaSessionConnection.queueItems + val playerVolume: Flow = MediaSessionConnection.isConnected.flatMapLatest { + MediaSessionConnection.binder?.songPlayer?.playerVolume ?: emptyFlow() + } val currentSong = mediaMetadata.flatMapLatest { mediaMetadata -> SongRepository.getSongById(mediaMetadata?.getString(METADATA_KEY_MEDIA_ID)).flow } diff --git a/app/src/main/res/layout/dialog_song_details.xml b/app/src/main/res/layout/dialog_song_details.xml index b7a3a2cf0..2eb0c1327 100644 --- a/app/src/main/res/layout/dialog_song_details.xml +++ b/app/src/main/res/layout/dialog_song_details.xml @@ -154,6 +154,24 @@ android:textSize="18sp" tools:text="4.22 dB" /> + + + + Low Persistent queue Skip silence + Audio normalization Equalizer Cache @@ -124,6 +125,7 @@ Bitrate Sample rate Loudness + Volume File size Unknown diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 78d6f2075..b1130ab6c 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -40,6 +40,7 @@ Low Persistent queue Skip silence + Audio normalization Equalizer Cache @@ -124,6 +125,7 @@ Bitrate Sample rate Loudness + Volume File size Unknown diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index 5ad868490..22b161f85 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -40,6 +40,7 @@ پایین Persistent queue Skip silence + Audio normalization اکولایزر Cache @@ -124,6 +125,7 @@ Bitrate Sample rate Loudness + Volume File size Unknown diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 62c2e7d37..072c39af8 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -40,6 +40,7 @@ Low Persistent queue Skip silence + Audio normalization Equalizer Cache @@ -124,6 +125,7 @@ Bitrate Sample rate Loudness + Volume File size Unknown diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index ad2a1ecc7..83ff0ee78 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -40,6 +40,7 @@ Low Persistent queue Skip silence + Audio normalization Equalizer Cache @@ -124,6 +125,7 @@ Bitrate Sample rate Loudness + Volume File size Unknown diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 8cf3a7510..381379974 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -40,6 +40,7 @@ Gyenge Állandó várósor Skip silence + Audio normalization Equalizer Gyorsítótár @@ -124,6 +125,7 @@ Bitráta Mintavétel Hangerősség + Volume Fájl méret Ismeretlen diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 44ee6407e..02bd75a1b 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -40,6 +40,7 @@ Bassa Coda persistente Skip silence + Audio normalization Equalizzatore Cache @@ -124,6 +125,7 @@ Bitrate Frequenza di campionamento Volume + Volume Dimensioni Sconosciuto diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 9e92562d9..824258e72 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -40,6 +40,7 @@ Persistent queue Skip silence + Audio normalization イコライザー Cache @@ -124,6 +125,7 @@ Bitrate Sample rate Loudness + Volume File size Unknown diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 06bfe4cbc..1a1b6419f 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -40,6 +40,7 @@ Low Persistent queue Skip silence + Audio normalization Equalizer Cache @@ -124,6 +125,7 @@ Bitrate Sample rate Loudness + Volume File size Unknown diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml index d35024b76..bebabba56 100644 --- a/app/src/main/res/values-ml-rIN/strings.xml +++ b/app/src/main/res/values-ml-rIN/strings.xml @@ -40,6 +40,7 @@ താഴ്ന്ന Persistent queue Skip silence + Audio normalization ഇക്വലൈസർ Cache @@ -124,6 +125,7 @@ Bitrate Sample rate Loudness + Volume File size Unknown diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 011aba5f1..7a54201fe 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -40,6 +40,7 @@ Baixa Fila persistente Skip silence + Audio normalization Equalizador Cache @@ -124,6 +125,7 @@ Bitrate Sample rate Loudness + Volume Tamanho do arquivo Desconhecido diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 4000951fe..e476f1cee 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -40,6 +40,7 @@ Low Persistent queue Skip silence + Audio normalization Equalizer Cache @@ -124,6 +125,7 @@ Bitrate Sample rate Loudness + Volume File size Unknown diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b7169e675..e4b2e2bd4 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -40,6 +40,7 @@ 保留播放队列 Skip silence + Audio normalization 均衡器 缓存 @@ -124,6 +125,7 @@ 比特率 采样率 音量 + Volume 文件大小 未知 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index b1f186e53..2f7d9c40d 100755 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -40,6 +40,7 @@ 保留播放佇列 跳過歌曲頭尾無聲片段 + 標準化音量 等化器 快取 @@ -123,7 +124,8 @@ 編碼 位元速率 採樣率 - 音量 + 響度 + 音量 檔案大小 未知 diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index abe051335..6484b47e2 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -67,6 +67,7 @@ PLAYLIST_SORT_DESC SHOW_LYRICS SKIP_SILENCE + AUDIO_NORMALIZE diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c11c77be1..7d545485f 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,6 +40,7 @@ Low Persistent queue Skip silence + Audio normalization Equalizer Cache @@ -124,6 +125,7 @@ Bitrate Sample rate Loudness + Volume File size Unknown diff --git a/app/src/main/res/xml/pref_player_audio.xml b/app/src/main/res/xml/pref_player_audio.xml index 6ce2d52ef..cf6121666 100644 --- a/app/src/main/res/xml/pref_player_audio.xml +++ b/app/src/main/res/xml/pref_player_audio.xml @@ -17,6 +17,10 @@ android:defaultValue="true" android:key="@string/pref_skip_silence" android:title="@string/pref_skip_silence_title" /> + Date: Wed, 26 Oct 2022 18:11:41 +0800 Subject: [PATCH 34/95] Add wake lock for player --- app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt index ab12b7321..578448005 100644 --- a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt +++ b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt @@ -21,6 +21,7 @@ import androidx.core.net.toUri import androidx.core.util.component1 import androidx.core.util.component2 import com.google.android.exoplayer2.* +import com.google.android.exoplayer2.C.WAKE_MODE_NETWORK import com.google.android.exoplayer2.PlaybackException.* import com.google.android.exoplayer2.Player.* import com.google.android.exoplayer2.Player.State @@ -144,11 +145,12 @@ class SongPlayer( val cache = SimpleCache(context.cacheDir.resolve("exoplayer"), cacheEvictor, StandaloneDatabaseProvider(context)) val player: ExoPlayer = ExoPlayer.Builder(context) .setMediaSourceFactory(createMediaSourceFactory()) + .setHandleAudioBecomingNoisy(true) + .setWakeMode(WAKE_MODE_NETWORK) .setAudioAttributes(AudioAttributes.Builder() .setUsage(C.USAGE_MEDIA) .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) .build(), true) - .setHandleAudioBecomingNoisy(true) .build() .apply { addListener(this@SongPlayer) From c257f53179398688db923d3bd7b22309db44c052 Mon Sep 17 00:00:00 2001 From: Sdarfeesh <50188628+Sdarfeesh@users.noreply.github.com> Date: Wed, 26 Oct 2022 20:54:04 +0800 Subject: [PATCH 35/95] Update Simplified Chinese Translation --- app/src/main/res/values-zh-rCN/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index e4b2e2bd4..e25344c8d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -39,8 +39,8 @@ 保留播放队列 - Skip silence - Audio normalization + 跳过歌曲头尾无声片段 + 标准化音量 均衡器 缓存 From 1065c276b14a3295ab60b370c0cb65e49e9f935f Mon Sep 17 00:00:00 2001 From: Sdarfeesh <50188628+Sdarfeesh@users.noreply.github.com> Date: Wed, 26 Oct 2022 21:20:34 +0800 Subject: [PATCH 36/95] Update strings.xml --- app/src/main/res/values-zh-rCN/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index e25344c8d..0f2032f42 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -125,7 +125,7 @@ 比特率 采样率 音量 - Volume + 音量 文件大小 未知 From 56c5cb12530fad7239ecdd3cfc42927cd1e62244 Mon Sep 17 00:00:00 2001 From: Sdarfeesh <50188628+Sdarfeesh@users.noreply.github.com> Date: Wed, 26 Oct 2022 21:24:38 +0800 Subject: [PATCH 37/95] Update strings.xml --- app/src/main/res/values-zh-rCN/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 0f2032f42..f7d6743d6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -124,7 +124,7 @@ 编码 比特率 采样率 - 音量 + 响度 音量 文件大小 未知 From d6cb07dcd5b051bdd0145870e11d585aaf4e49ed Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Wed, 26 Oct 2022 21:27:12 +0800 Subject: [PATCH 38/95] Fix #362 --- .../music/ui/fragments/youtube/YouTubeSuggestionFragment.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/youtube/YouTubeSuggestionFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/youtube/YouTubeSuggestionFragment.kt index 16662acd7..61a29a151 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/youtube/YouTubeSuggestionFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/youtube/YouTubeSuggestionFragment.kt @@ -1,6 +1,7 @@ package com.zionhuang.music.ui.fragments.youtube import android.app.Activity.RESULT_OK +import android.content.ActivityNotFoundException import android.content.Intent import android.os.Bundle import android.speech.RecognizerIntent @@ -83,7 +84,10 @@ class YouTubeSuggestionFragment : NavigationFragment Date: Wed, 26 Oct 2022 22:24:21 +0800 Subject: [PATCH 39/95] Fix minimum silence duration too short --- .../zionhuang/music/playback/SongPlayer.kt | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt index 578448005..87685bb1a 100644 --- a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt +++ b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt @@ -28,7 +28,9 @@ import com.google.android.exoplayer2.Player.State import com.google.android.exoplayer2.analytics.AnalyticsListener import com.google.android.exoplayer2.analytics.PlaybackStats import com.google.android.exoplayer2.analytics.PlaybackStatsListener -import com.google.android.exoplayer2.audio.AudioAttributes +import com.google.android.exoplayer2.audio.* +import com.google.android.exoplayer2.audio.DefaultAudioSink.* +import com.google.android.exoplayer2.audio.SilenceSkippingAudioProcessor.DEFAULT_SILENCE_THRESHOLD_LEVEL import com.google.android.exoplayer2.database.StandaloneDatabaseProvider import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.ext.mediasession.TimelineQueueEditor.* @@ -145,6 +147,7 @@ class SongPlayer( val cache = SimpleCache(context.cacheDir.resolve("exoplayer"), cacheEvictor, StandaloneDatabaseProvider(context)) val player: ExoPlayer = ExoPlayer.Builder(context) .setMediaSourceFactory(createMediaSourceFactory()) + .setRenderersFactory(createRenderersFactory()) .setHandleAudioBecomingNoisy(true) .setWakeMode(WAKE_MODE_NETWORK) .setAudioAttributes(AudioAttributes.Builder() @@ -423,8 +426,6 @@ class SongPlayer( startIndex = queue.mediaItemIndex, position = queue.position ), playWhenReady = false) - }.onFailure { e -> - e.printStackTrace() } } } @@ -502,6 +503,21 @@ class SongPlayer( } }) + private fun createRenderersFactory() = object : DefaultRenderersFactory(context) { + override fun buildAudioSink(context: Context, enableFloatOutput: Boolean, enableAudioTrackPlaybackParams: Boolean, enableOffload: Boolean) = + DefaultAudioSink.Builder() + .setAudioCapabilities(AudioCapabilities.getCapabilities(context)) + .setEnableFloatOutput(enableFloatOutput) + .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams) + .setOffloadMode(if (enableOffload) OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED else OFFLOAD_MODE_DISABLED) + .setAudioProcessorChain(DefaultAudioProcessorChain( + emptyArray(), + SilenceSkippingAudioProcessor(2_000_000, 20_000, DEFAULT_SILENCE_THRESHOLD_LEVEL), + SonicAudioProcessor() + )) + .build() + } + fun playQueue(queue: Queue, playWhenReady: Boolean = true) { currentQueue = queue mediaSession.setQueueTitle(null) From ddc152a561c65c7f9db29759339f6d84e2011797 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Wed, 26 Oct 2022 22:42:58 +0800 Subject: [PATCH 40/95] Fix lyrics normalization method --- .../main/java/com/zionhuang/kugou/KuGou.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt b/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt index 63f4c3ec2..609b6ab20 100644 --- a/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt +++ b/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt @@ -17,6 +17,7 @@ import io.ktor.util.* import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import java.lang.Character.UnicodeScript +import java.lang.Integer.min import kotlin.math.abs /** @@ -124,7 +125,16 @@ object KuGou { private fun String.toTraditionalChinese(useTraditionalChinese: Boolean) = if (useTraditionalChinese) ZhConverterUtil.toTraditional(this) else this private fun String.normalize(title: String, artist: String): String = lines().filterNot { line -> - line.endsWith("]") || BANNED_WORDS.any { line.contains(it) } || line.endsWith("].") + line.endsWith("]") + }.let { + var cutLine = 0 + for (i in min(30, it.lastIndex) downTo 0) { + if (it[i] matches BANNED_REGEX) { + cutLine = i + 1 + break + } + } + it.drop(cutLine) }.let { lines -> val firstLine = lines.firstOrNull()?.toSimplifiedChinese() ?: return@let lines if (title.toSimplifiedChinese() in firstLine || @@ -138,17 +148,7 @@ object KuGou { UnicodeScript.of(c.code) in JapaneseUnicodeScript }) - private val BANNED_WORDS = listOf( - "]词:", "]词:", "]作词:", "]作词:", - "]曲:", "]曲:", "]作曲:", "]作曲:", - "]编曲:", "]编曲 Arrangement:", - "]Producer:", "]制作人 Producer:", - "]Drums:", - "]Guitar:", - "]Strings:", - "]Mixer:", - "]Mastering Engineer:" - ) + private val BANNED_REGEX = ".+].+[::].+".toRegex() private val JapaneseUnicodeScript = hashSetOf( UnicodeScript.HIRAGANA, From fe394fb7601511dc6fff02a726acc1d2f090ce47 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Wed, 26 Oct 2022 23:39:12 +0800 Subject: [PATCH 41/95] Enhance LyricsView --- .../ui/fragments/BottomControlsFragment.kt | 5 +-- .../zionhuang/music/ui/widgets/LyricsView.kt | 31 ++++++++++--------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt index c08783577..2b43a0e40 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt @@ -1,7 +1,7 @@ package com.zionhuang.music.ui.fragments import android.os.Bundle -import android.support.v4.media.session.PlaybackStateCompat +import android.support.v4.media.session.PlaybackStateCompat.* import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -94,11 +94,12 @@ class BottomControlsFragment : Fragment() { } lifecycleScope.launch { viewModel.playbackState.collect { playbackState -> - if (playbackState.state != PlaybackStateCompat.STATE_NONE && playbackState.state != PlaybackStateCompat.STATE_STOPPED) { + if (playbackState.state != STATE_NONE && playbackState.state != STATE_STOPPED) { if (mainActivity.bottomSheetBehavior.state == STATE_HIDDEN) { mainActivity.bottomSheetBehavior.state = STATE_COLLAPSED } } + binding.lyricsView.isPlaying = playbackState.state == STATE_PLAYING || playbackState.state == STATE_BUFFERING } } lifecycleScope.launch { diff --git a/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt b/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt index 399119aa7..e3c23ad51 100644 --- a/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt +++ b/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt @@ -11,6 +11,7 @@ import android.text.StaticLayout import android.text.TextPaint import android.text.format.DateUtils.SECOND_IN_MILLIS import android.util.AttributeSet +import android.util.Log import android.view.GestureDetector import android.view.GestureDetector.SimpleOnGestureListener import android.view.MotionEvent @@ -67,9 +68,17 @@ class LyricsView @JvmOverloads constructor( private var isFling = false private var textGravity = 0 // left/center/right private var isSyncedLyrics = true + var isPlaying = false + set(value) { + if (field == value) return + field = value + if (value && !isFling && !inActive) { + smoothScrollTo(currentLine) + } + } private val hideTimelineRunnable = Runnable { - if (hasLyrics() && isSyncedLyrics && inActive) { - inActive = false + inActive = false + if (hasLyrics() && isSyncedLyrics && isPlaying) { smoothScrollTo(currentLine) } } @@ -121,10 +130,10 @@ class LyricsView @JvmOverloads constructor( if (onLyricsClickListener?.onLyricsClick(lineTime) == true) { inActive = false removeCallbacks(hideTimelineRunnable) - previousLine = if (line != currentLine) line else -1 - currentLine = line - updateCurrentTextSize() - smoothScrollTo(line) +// previousLine = if (line != currentLine) line else -1 +// currentLine = line +// updateCurrentTextSize() +// smoothScrollTo(line) return true } } else { @@ -249,6 +258,7 @@ class LyricsView @JvmOverloads constructor( fun hasLyrics(): Boolean = lrcEntryList.isNotEmpty() fun updateTime(time: Long, animate: Boolean = true) { + Log.d("DBG", "update time: $time, animate: $animate") runOnUi { if (!hasLyrics() || !isSyncedLyrics) { return@runOnUi @@ -323,7 +333,7 @@ class LyricsView @JvmOverloads constructor( } } - fun updateCurrentTextSize(animate: Boolean = true) { + private fun updateCurrentTextSize(animate: Boolean = true) { if (animate) { ValueAnimator.ofFloat(normalTextSize, currentTextSize).apply { interpolator = FastOutSlowInInterpolator() @@ -344,7 +354,6 @@ class LyricsView @JvmOverloads constructor( if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) { isTouching = false if (hasLyrics() && isSyncedLyrics && !isFling) { - adjustCenter() postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME) } } @@ -359,7 +368,6 @@ class LyricsView @JvmOverloads constructor( if (isFling && scroller.isFinished) { isFling = false if (hasLyrics() && isSyncedLyrics && !isTouching) { - adjustCenter() postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME) } } @@ -402,10 +410,6 @@ class LyricsView @JvmOverloads constructor( invalidate() } - private fun adjustCenter() { - smoothScrollTo(getCenterLine(), ADJUST_DURATION) - } - private fun smoothScrollTo(line: Int, duration: Long = animationDuration) { val endOffset = getOffset(line) endAnimation() @@ -485,7 +489,6 @@ class LyricsView @JvmOverloads constructor( } companion object { - private const val ADJUST_DURATION = 300L private const val TIMELINE_KEEP_TIME = 4 * SECOND_IN_MILLIS } From e865c6e944af27fcea186d38384877d36eba9226 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Wed, 26 Oct 2022 23:44:04 +0800 Subject: [PATCH 42/95] Enhance LyricsView (#76) --- .../main/java/com/zionhuang/music/ui/widgets/LyricsView.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt b/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt index e3c23ad51..1c8ea257a 100644 --- a/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt +++ b/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt @@ -268,7 +268,10 @@ class LyricsView @JvmOverloads constructor( previousLine = currentLine currentLine = line updateCurrentTextSize(animate) - if (!isFling && !inActive) { + if (!animate) { + scroller.forceFinished(true) + } + if ((!isFling && !inActive) || !animate) { // !animate means the user is dragging the seekbar smoothScrollTo(line, if (animate) animationDuration else 0) } } From d7a6f5d862442a11d8a75c316264153e0f79c07c Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Thu, 27 Oct 2022 08:33:28 +0800 Subject: [PATCH 43/95] Fix #363 --- app/src/main/res/values-fr-rFR/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 83ff0ee78..a284e23c5 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -213,6 +213,7 @@ %d sélectionné %d sélectionné + %d sélectionné Annuler Can\'t identify this url. From 8b610bd0fd4db502a0ab837ac5411ce77a723e30 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Thu, 27 Oct 2022 09:31:08 +0800 Subject: [PATCH 44/95] Fix #360 --- app/src/main/java/com/zionhuang/music/App.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/zionhuang/music/App.kt b/app/src/main/java/com/zionhuang/music/App.kt index 6be104734..5e5bbe99f 100644 --- a/app/src/main/java/com/zionhuang/music/App.kt +++ b/app/src/main/java/com/zionhuang/music/App.kt @@ -1,6 +1,7 @@ package com.zionhuang.music import android.app.Application +import android.os.Build import android.util.Log import android.widget.Toast import android.widget.Toast.LENGTH_SHORT @@ -66,6 +67,7 @@ class App : Application(), ImageLoaderFactory { override fun newImageLoader() = ImageLoader.Builder(this) .crossfade(true) .respectCacheHeaders(false) + .allowHardware(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) .diskCache( DiskCache.Builder() .directory(cacheDir.resolve("coil")) From 72847e6e6eb3df3a40899fda697c118a8c147966 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Thu, 27 Oct 2022 15:50:27 +0800 Subject: [PATCH 45/95] Update project name --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 325ee702a..14480deb2 100755 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,4 @@ -rootProject.name = "Music" +rootProject.name = "InnerTune" include(":app") include(":innertube") include(":kugou") From 72751b4c635e72383a44101b59f3455a7643e3f2 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Thu, 27 Oct 2022 16:22:44 +0800 Subject: [PATCH 46/95] Refactor code --- .../download/DownloadBroadcastReceiver.kt | 2 +- .../zionhuang/music/playback/MusicService.kt | 23 ++++---- .../zionhuang/music/playback/SongPlayer.kt | 54 +++++++++---------- .../zionhuang/music/repos/SongRepository.kt | 4 +- .../music/repos/YouTubeRepository.kt | 7 ++- .../music/ui/activities/MainActivity.kt | 2 +- .../music/ui/fragments/QueueSheetFragment.kt | 7 +-- .../fragments/dialogs/CreatePlaylistDialog.kt | 2 +- .../ui/fragments/dialogs/EditArtistDialog.kt | 5 +- .../fragments/dialogs/EditPlaylistDialog.kt | 2 +- .../ui/fragments/dialogs/EditSongDialog.kt | 2 +- .../settings/PrivacySettingsFragment.kt | 2 +- .../ui/fragments/songs/ArtistSongsFragment.kt | 2 +- .../fragments/songs/PlaylistSongsFragment.kt | 7 +-- .../youtube/YouTubeSuggestionFragment.kt | 2 +- .../music/ui/listeners/AlbumMenuListener.kt | 10 ++-- .../music/ui/listeners/ArtistMenuListener.kt | 8 +-- .../listeners/CustomPlaylistMenuListener.kt | 14 +++-- .../ui/listeners/PlaylistMenuListener.kt | 12 +++-- .../music/ui/listeners/SongMenuListener.kt | 14 ++--- .../ui/listeners/YTItemBatchMenuListener.kt | 9 ++-- .../ui/viewholders/LocalItemViewHolder.kt | 4 +- .../music/ui/viewholders/YouTubeViewHolder.kt | 2 +- .../zionhuang/music/ui/widgets/LyricsView.kt | 2 - .../music/utils/NavigationEndpointHandler.kt | 11 ++-- .../music/viewmodels/LocalSearchViewModel.kt | 11 ++-- .../music/viewmodels/PlaybackViewModel.kt | 7 +-- .../music/viewmodels/SongsViewModel.kt | 30 ++++++----- .../music/viewmodels/SuggestionViewModel.kt | 10 ++-- .../viewmodels/YouTubeBrowseViewModel.kt | 3 +- .../viewmodels/YouTubeSearchViewModel.kt | 5 +- 31 files changed, 151 insertions(+), 124 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/download/DownloadBroadcastReceiver.kt b/app/src/main/java/com/zionhuang/music/download/DownloadBroadcastReceiver.kt index 62ecfb6b3..7bb930546 100644 --- a/app/src/main/java/com/zionhuang/music/download/DownloadBroadcastReceiver.kt +++ b/app/src/main/java/com/zionhuang/music/download/DownloadBroadcastReceiver.kt @@ -17,7 +17,7 @@ class DownloadBroadcastReceiver : BroadcastReceiver() { @OptIn(DelicateCoroutinesApi::class) override fun onReceive(context: Context, intent: Intent) { val downloadManager = context.getSystemService()!! - val songRepository = SongRepository + val songRepository = SongRepository(context) when (intent.action) { ACTION_DOWNLOAD_COMPLETE -> { diff --git a/app/src/main/java/com/zionhuang/music/playback/MusicService.kt b/app/src/main/java/com/zionhuang/music/playback/MusicService.kt index 07810f2f1..2db79422a 100644 --- a/app/src/main/java/com/zionhuang/music/playback/MusicService.kt +++ b/app/src/main/java/com/zionhuang/music/playback/MusicService.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.runBlocking class MusicService : LifecycleMediaBrowserService() { private val binder = MusicBinder() + private val songRepository by lazy { SongRepository(this) } private lateinit var songPlayer: SongPlayer override fun onCreate() { @@ -102,52 +103,52 @@ class MusicService : LifecycleMediaBrowserService() { )) SONG -> { result.detach() - result.sendResult(SongRepository.getAllSongs(SongSortInfoPreference).flow.first().map { + result.sendResult(songRepository.getAllSongs(SongSortInfoPreference).flow.first().map { MediaBrowserCompat.MediaItem(it.toMediaMetadata().copy(id = "$parentId/${it.id}").toMediaDescription(this@MusicService), FLAG_PLAYABLE) }.toMutableList()) } ARTIST -> { result.detach() - result.sendResult(SongRepository.getAllArtists(ArtistSortInfoPreference).flow.first().map { artist -> + result.sendResult(songRepository.getAllArtists(ArtistSortInfoPreference).flow.first().map { artist -> mediaBrowserItem("$ARTIST/${artist.id}", artist.artist.name, resources.getQuantityString(R.plurals.song_count, artist.songCount, artist.songCount), artist.artist.thumbnailUrl?.toUri()) }.toMutableList()) } ALBUM -> { result.detach() - result.sendResult(SongRepository.getAllAlbums(AlbumSortInfoPreference).flow.first().map { album -> + result.sendResult(songRepository.getAllAlbums(AlbumSortInfoPreference).flow.first().map { album -> mediaBrowserItem("$ALBUM/${album.id}", album.album.title, album.artists.joinToString(), album.album.thumbnailUrl?.toUri()) }.toMutableList()) } PLAYLIST -> { result.detach() - val likedSongCount = SongRepository.getLikedSongCount().first() - val downloadedSongCount = SongRepository.getDownloadedSongCount().first() + val likedSongCount = songRepository.getLikedSongCount().first() + val downloadedSongCount = songRepository.getDownloadedSongCount().first() result.sendResult((listOf( mediaBrowserItem("$PLAYLIST/$LIKED_PLAYLIST_ID", getString(R.string.liked_songs), resources.getQuantityString(R.plurals.song_count, likedSongCount, likedSongCount), drawableUri(R.drawable.ic_favorite)), mediaBrowserItem("$PLAYLIST/$DOWNLOADED_PLAYLIST_ID", getString(R.string.downloaded_songs), resources.getQuantityString(R.plurals.song_count, downloadedSongCount, downloadedSongCount), drawableUri(R.drawable.ic_save_alt)) - ) + SongRepository.getAllPlaylists(PlaylistSortInfoPreference).flow.first().filter { it.playlist.isLocalPlaylist }.map { playlist -> + ) + songRepository.getAllPlaylists(PlaylistSortInfoPreference).flow.first().filter { it.playlist.isLocalPlaylist }.map { playlist -> mediaBrowserItem("$PLAYLIST/${playlist.id}", playlist.playlist.name, resources.getQuantityString(R.plurals.song_count, playlist.songCount, playlist.songCount), playlist.playlist.thumbnailUrl?.toUri() ?: playlist.thumbnails.firstOrNull()?.toUri()) }).toMutableList()) } else -> when { parentId.startsWith("$ARTIST/") -> { result.detach() - result.sendResult(SongRepository.getArtistSongs(parentId.removePrefix("$ARTIST/"), SongSortInfoPreference).flow.first().map { + result.sendResult(songRepository.getArtistSongs(parentId.removePrefix("$ARTIST/"), SongSortInfoPreference).flow.first().map { MediaBrowserCompat.MediaItem(it.toMediaMetadata().copy(id = "$parentId/${it.id}").toMediaDescription(this@MusicService), FLAG_PLAYABLE) }.toMutableList()) } parentId.startsWith("$ALBUM/") -> { result.detach() - result.sendResult(SongRepository.getAlbumSongs(parentId.removePrefix("$ALBUM/")).map { + result.sendResult(songRepository.getAlbumSongs(parentId.removePrefix("$ALBUM/")).map { MediaBrowserCompat.MediaItem(it.toMediaMetadata().copy(id = "$parentId/${it.id}").toMediaDescription(this@MusicService), FLAG_PLAYABLE) }.toMutableList()) } parentId.startsWith("$PLAYLIST/") -> { result.detach() result.sendResult(when (val playlistId = parentId.removePrefix("$PLAYLIST/")) { - LIKED_PLAYLIST_ID -> SongRepository.getLikedSongs(SongSortInfoPreference) - DOWNLOADED_PLAYLIST_ID -> SongRepository.getDownloadedSongs(SongSortInfoPreference) - else -> SongRepository.getPlaylistSongs(playlistId) + LIKED_PLAYLIST_ID -> songRepository.getLikedSongs(SongSortInfoPreference) + DOWNLOADED_PLAYLIST_ID -> songRepository.getDownloadedSongs(SongSortInfoPreference) + else -> songRepository.getPlaylistSongs(playlistId) }.flow.first().map { MediaBrowserCompat.MediaItem(it.toMediaMetadata().copy(id = "$parentId/${it.id}").toMediaDescription(this@MusicService), FLAG_PLAYABLE) }.toMutableList()) diff --git a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt index 87685bb1a..a93588a76 100644 --- a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt +++ b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt @@ -112,7 +112,7 @@ class SongPlayer( private val scope: CoroutineScope, notificationListener: PlayerNotificationManager.NotificationListener, ) : Listener, PlaybackStatsListener.Callback { - private val localRepository = SongRepository + private val songRepository = SongRepository(context) private val connectivityManager = context.getSystemService()!! private val bitmapProvider = BitmapProvider(context) @@ -124,10 +124,10 @@ class SongPlayer( val playerVolume = MutableStateFlow(1f) val currentMediaMetadata = MutableStateFlow(null) private val currentSongFlow = currentMediaMetadata.flatMapLatest { mediaMetadata -> - localRepository.getSongById(mediaMetadata?.id).flow + songRepository.getSongById(mediaMetadata?.id).flow } private val currentFormat = currentMediaMetadata.flatMapLatest { mediaMetadata -> - localRepository.getSongFormat(mediaMetadata?.id).flow + songRepository.getSongFormat(mediaMetadata?.id).flow } var currentSong: Song? = null @@ -171,7 +171,7 @@ class SongPlayer( when (path.firstOrNull()) { SONG -> { val songId = path.getOrNull(1) ?: return@launch - val allSongs = SongRepository.getAllSongs(SongSortInfoPreference).flow.first() + val allSongs = songRepository.getAllSongs(SongSortInfoPreference).flow.first() playQueue(ListQueue( title = context.getString(R.string.queue_all_songs), items = allSongs.map { it.toMediaItem() }, @@ -181,8 +181,8 @@ class SongPlayer( ARTIST -> { val songId = path.getOrNull(2) ?: return@launch val artistId = path.getOrNull(1) ?: return@launch - val artist = SongRepository.getArtistById(artistId) ?: return@launch - val songs = SongRepository.getArtistSongs(artistId, SongSortInfoPreference).flow.first() + val artist = songRepository.getArtistById(artistId) ?: return@launch + val songs = songRepository.getArtistSongs(artistId, SongSortInfoPreference).flow.first() playQueue(ListQueue( title = artist.name, items = songs.map { it.toMediaItem() }, @@ -192,8 +192,8 @@ class SongPlayer( ALBUM -> { val songId = path.getOrNull(2) ?: return@launch val albumId = path.getOrNull(1) ?: return@launch - val album = SongRepository.getAlbum(albumId) ?: return@launch - val songs = SongRepository.getAlbumSongs(albumId) + val album = songRepository.getAlbum(albumId) ?: return@launch + val songs = songRepository.getAlbumSongs(albumId) playQueue(ListQueue( title = album.title, items = songs.map { it.toMediaItem() }, @@ -204,15 +204,15 @@ class SongPlayer( val songId = path.getOrNull(2) ?: return@launch val playlistId = path.getOrNull(1) ?: return@launch val songs = when (playlistId) { - LIKED_PLAYLIST_ID -> SongRepository.getLikedSongs(SongSortInfoPreference).flow.first() - DOWNLOADED_PLAYLIST_ID -> SongRepository.getDownloadedSongs(SongSortInfoPreference).flow.first() - else -> SongRepository.getPlaylistSongs(playlistId).getList() + LIKED_PLAYLIST_ID -> songRepository.getLikedSongs(SongSortInfoPreference).flow.first() + DOWNLOADED_PLAYLIST_ID -> songRepository.getDownloadedSongs(SongSortInfoPreference).flow.first() + else -> songRepository.getPlaylistSongs(playlistId).getList() } playQueue(ListQueue( title = when (playlistId) { LIKED_PLAYLIST_ID -> context.getString(R.string.liked_songs) DOWNLOADED_PLAYLIST_ID -> context.getString(R.string.downloaded_songs) - else -> SongRepository.getPlaylistById(playlistId).playlist.name + else -> songRepository.getPlaylistById(playlistId).playlist.name }, items = songs.map { it.toMediaItem() }, startIndex = songs.indexOfFirst { it.id == songId }.takeIf { it != -1 } ?: 0 @@ -366,14 +366,14 @@ class SongPlayer( lyricProviders.forEach { provider -> if (provider.isEnabled(context)) { provider.getLyrics(mediaMetadata.id, mediaMetadata.title, mediaMetadata.artists.joinToString { it.name }, mediaMetadata.duration).onSuccess { lyrics -> - localRepository.upsert(LyricsEntity(mediaMetadata.id, lyrics)) + songRepository.upsert(LyricsEntity(mediaMetadata.id, lyrics)) return }.onFailure { it.printStackTrace() } } } - localRepository.upsert(LyricsEntity(mediaMetadata.id, LYRICS_NOT_FOUND)) + songRepository.upsert(LyricsEntity(mediaMetadata.id, LYRICS_NOT_FOUND)) } init { @@ -391,7 +391,7 @@ class SongPlayer( combine(currentMediaMetadata.distinctUntilChangedBy { it?.id }, showLyrics) { mediaMetadata, showLyrics -> Pair(mediaMetadata, showLyrics) }.collectLatest { (mediaMetadata, showLyrics) -> - if (showLyrics && mediaMetadata != null && !localRepository.hasLyrics(mediaMetadata.id)) { + if (showLyrics && mediaMetadata != null && !songRepository.hasLyrics(mediaMetadata.id)) { loadLyrics(mediaMetadata) } } @@ -452,9 +452,9 @@ class SongPlayer( // Check whether format exists so that users from older version can view format details // There may be inconsistent between the downloaded file and the displayed info if user change audio quality frequently - val playedFormat = localRepository.getSongFormat(mediaId).getValueAsync() - if (playedFormat != null && localRepository.getSongById(mediaId).getValueAsync()?.song?.downloadState == STATE_DOWNLOADED) { - return@runBlocking dataSpec.withUri(localRepository.getSongFile(mediaId).toUri()) + val playedFormat = songRepository.getSongFormat(mediaId).getValueAsync() + if (playedFormat != null && songRepository.getSongById(mediaId).getValueAsync()?.song?.downloadState == STATE_DOWNLOADED) { + return@runBlocking dataSpec.withUri(songRepository.getSongFile(mediaId).toUri()) } withContext(IO) { @@ -479,7 +479,7 @@ class SongPlayer( } } } ?: throw PlaybackException(context.getString(R.string.error_no_stream), null, ERROR_CODE_NO_STREAM) - localRepository.upsert(FormatEntity( + songRepository.upsert(FormatEntity( id = mediaId, itag = format.itag, mimeType = format.mimeType.split(";")[0], @@ -598,9 +598,9 @@ class SongPlayer( val song = currentSong val mediaMetadata = currentMediaMetadata.value ?: return@launch if (song == null) { - localRepository.addSong(mediaMetadata) + songRepository.addSong(mediaMetadata) } else { - localRepository.deleteSong(song) + songRepository.deleteSong(song) } } } @@ -610,19 +610,19 @@ class SongPlayer( val song = currentSong val mediaMetadata = currentMediaMetadata.value ?: return@launch if (song == null) { - localRepository.addSong(mediaMetadata) - localRepository.getSongById(mediaMetadata.id).getValueAsync()?.let { - localRepository.toggleLiked(it) + songRepository.addSong(mediaMetadata) + songRepository.getSongById(mediaMetadata.id).getValueAsync()?.let { + songRepository.toggleLiked(it) } } else { - localRepository.toggleLiked(song) + songRepository.toggleLiked(song) } } } private fun addToLibrary(mediaMetadata: MediaMetadata) { scope.launch(context.exceptionHandler) { - localRepository.addSong(mediaMetadata) + songRepository.addSong(mediaMetadata) } } @@ -690,7 +690,7 @@ class SongPlayer( override fun onPlaybackStatsReady(eventTime: AnalyticsListener.EventTime, playbackStats: PlaybackStats) { val mediaItem = eventTime.timeline.getWindow(eventTime.windowIndex, Timeline.Window()).mediaItem scope.launch { - localRepository.incrementSongTotalPlayTime(mediaItem.mediaId, playbackStats.totalPlayTimeMs) + songRepository.incrementSongTotalPlayTime(mediaItem.mediaId, playbackStats.totalPlayTimeMs) } } diff --git a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt index 8abd08a79..48a2427c9 100644 --- a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt +++ b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt @@ -1,6 +1,7 @@ package com.zionhuang.music.repos import android.app.DownloadManager +import android.content.Context import android.net.ConnectivityManager import androidx.core.content.getSystemService import androidx.core.net.toUri @@ -37,8 +38,7 @@ import kotlinx.coroutines.withContext import java.io.File import java.time.LocalDateTime -object SongRepository : LocalRepository { - private val context = getApplication() +class SongRepository(val context: Context) : LocalRepository { private val database = MusicDatabase.getInstance(context) private val songDao = database.songDao private val artistDao = database.artistDao diff --git a/app/src/main/java/com/zionhuang/music/repos/YouTubeRepository.kt b/app/src/main/java/com/zionhuang/music/repos/YouTubeRepository.kt index 95ff9092b..60570cbaa 100644 --- a/app/src/main/java/com/zionhuang/music/repos/YouTubeRepository.kt +++ b/app/src/main/java/com/zionhuang/music/repos/YouTubeRepository.kt @@ -1,5 +1,6 @@ package com.zionhuang.music.repos +import android.content.Context import androidx.paging.PagingSource import androidx.paging.PagingState import com.zionhuang.innertube.YouTube @@ -15,7 +16,9 @@ import com.zionhuang.music.utils.InfoCache.checkCache import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.withContext -object YouTubeRepository { +class YouTubeRepository(val context: Context) { + private val songRepository = SongRepository(context) + fun searchAll(query: String) = object : PagingSource, YTBaseItem>() { override suspend fun load(params: LoadParams>) = withContext(IO) { try { @@ -64,7 +67,7 @@ object YouTubeRepository { // inject library artist songs preview browseResult.copy( items = browseResult.items.toMutableList().apply { - addAll(if (browseResult.items.firstOrNull() is ArtistHeader) 1 else 0, SongRepository.getArtistSongsPreview(endpoint.browseId).getOrThrow()) + addAll(if (browseResult.items.firstOrNull() is ArtistHeader) 1 else 0, songRepository.getArtistSongsPreview(endpoint.browseId).getOrThrow()) } ) } else { diff --git a/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt b/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt index 34e5a48a8..5519a7c0d 100644 --- a/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt +++ b/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt @@ -156,7 +156,7 @@ class MainActivity : ThemedBindingActivity(), NavController } lifecycleScope.launch { - SongRepository.validateDownloads() + SongRepository(this@MainActivity).validateDownloads() } preferenceLiveData(R.string.pref_show_lyrics, false).observe(this) { showLyrics -> keepScreenOn(showLyrics && bottomSheetBehavior.state == STATE_EXPANDED) diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/QueueSheetFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/QueueSheetFragment.kt index 8e0df6375..798d16c41 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/QueueSheetFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/QueueSheetFragment.kt @@ -95,6 +95,7 @@ class QueueSheetFragment : Fragment() { } }) + private val songRepository by lazy { SongRepository(requireContext()) } private val adapter: QueueItemAdapter = QueueItemAdapter(itemTouchHelper) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { @@ -165,8 +166,8 @@ class QueueSheetFragment : Fragment() { val mainContent = mainActivity.binding.mainContent ChoosePlaylistDialog { playlist -> GlobalScope.launch(requireContext().exceptionHandler) { - if (song != null) SongRepository.addToPlaylist(playlist, song) - else SongRepository.addMediaItemToPlaylist(playlist, mediaMetadata) + if (song != null) songRepository.addToPlaylist(playlist, song) + else songRepository.addMediaItemToPlaylist(playlist, mediaMetadata) Snackbar.make(mainContent, getString(R.string.snackbar_added_to_playlist, playlist.name), BaseTransientBottomBar.LENGTH_SHORT) .setAction(R.string.snackbar_action_view) { mainActivity.currentFragment?.exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true).addTarget(R.id.fragment_content) @@ -178,7 +179,7 @@ class QueueSheetFragment : Fragment() { } R.id.action_download -> { GlobalScope.launch(requireContext().exceptionHandler) { - SongRepository.downloadSong(song?.song ?: SongRepository.addSong(mediaMetadata)) + songRepository.downloadSong(song?.song ?: songRepository.addSong(mediaMetadata)) } } R.id.action_view_artist -> { diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/CreatePlaylistDialog.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/CreatePlaylistDialog.kt index 63af667bb..02a74fbe3 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/CreatePlaylistDialog.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/CreatePlaylistDialog.kt @@ -67,7 +67,7 @@ class CreatePlaylistDialog() : AppCompatDialogFragment() { name = name ) GlobalScope.launch(requireContext().exceptionHandler) { - SongRepository.insertPlaylist(playlist) + SongRepository(requireContext()).insertPlaylist(playlist) listener?.invoke(playlist) } dismiss() diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/EditArtistDialog.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/EditArtistDialog.kt index ef4082b28..b570bd609 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/EditArtistDialog.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/EditArtistDialog.kt @@ -54,7 +54,8 @@ class EditArtistDialog : AppCompatDialogFragment() { if (binding.textInput.editText?.text.isNullOrEmpty()) return val name = binding.textInput.editText?.text.toString() GlobalScope.launch { - val existedArtist = SongRepository.getArtistByName(name) + val songRepository = SongRepository(requireContext()) + val existedArtist = songRepository.getArtistByName(name) if (existedArtist != null && existedArtist.id != artist.id) { // name exists withContext(Dispatchers.Main) { @@ -65,7 +66,7 @@ class EditArtistDialog : AppCompatDialogFragment() { .show() } } else { - SongRepository.updateArtist(artist.copy(name = name)) + songRepository.updateArtist(artist.copy(name = name)) dismiss() } } diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/EditPlaylistDialog.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/EditPlaylistDialog.kt index 50eb22c98..49c496e33 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/EditPlaylistDialog.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/EditPlaylistDialog.kt @@ -56,7 +56,7 @@ class EditPlaylistDialog : AppCompatDialogFragment() { if (binding.textInput.editText?.text.isNullOrEmpty()) return val name = binding.textInput.editText?.text.toString() GlobalScope.launch { - SongRepository.updatePlaylist(playlist.copy(name = name)) + SongRepository(requireContext()).updatePlaylist(playlist.copy(name = name)) } dismiss() } diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/EditSongDialog.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/EditSongDialog.kt index 3d404e34b..f885dd246 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/EditSongDialog.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/EditSongDialog.kt @@ -69,7 +69,7 @@ class EditSongDialog : AppCompatDialogFragment() { val title = binding.songTitle.editText?.text.toString() // TODO GlobalScope.launch { - SongRepository.updateSongTitle(song, title) + SongRepository(requireContext()).updateSongTitle(song, title) } dismiss() } diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/settings/PrivacySettingsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/settings/PrivacySettingsFragment.kt index 6b12d6c5f..206e2f937 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/settings/PrivacySettingsFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/settings/PrivacySettingsFragment.kt @@ -20,7 +20,7 @@ class PrivacySettingsFragment : BaseSettingsFragment() { .setMessage(R.string.clear_search_history_question) .setPositiveButton(android.R.string.ok) { _, _ -> GlobalScope.launch { - SongRepository.clearSearchHistory() + SongRepository(requireContext()).clearSearchHistory() } } .setNegativeButton(android.R.string.cancel, null) diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/songs/ArtistSongsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/songs/ArtistSongsFragment.kt index a03d388eb..76076c0d5 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/songs/ArtistSongsFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/songs/ArtistSongsFragment.kt @@ -88,7 +88,7 @@ class ArtistSongsFragment : RecyclerViewFragment() { } lifecycleScope.launch { - artist = SongRepository.getArtistById(args.artistId)!! + artist = SongRepository(requireContext()).getArtistById(args.artistId)!! requireAppCompatActivity().title = artist.name songsViewModel.getArtistSongsAsFlow(args.artistId).collectLatest { adapter.submitList(it) diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/songs/PlaylistSongsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/songs/PlaylistSongsFragment.kt index 8c72b7939..d478c3b48 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/songs/PlaylistSongsFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/songs/PlaylistSongsFragment.kt @@ -45,6 +45,7 @@ class PlaylistSongsFragment : RecyclerViewFragment() private val playbackViewModel by activityViewModels() private val songsViewModel by activityViewModels() + private val songRepository by lazy { SongRepository(requireContext()) } private val menuListener = SongMenuListener(this) override val adapter = DraggableLocalItemAdapter().apply { songMenuListener = menuListener @@ -73,7 +74,7 @@ class PlaylistSongsFragment : RecyclerViewFragment() ViewCompat.setElevation(viewHolder.itemView, 0f) lifecycleScope.launch { move?.let { - SongRepository.movePlaylistItems(playlistId, it.first, it.second) + songRepository.movePlaylistItems(playlistId, it.first, it.second) move = null } } @@ -90,7 +91,7 @@ class PlaylistSongsFragment : RecyclerViewFragment() override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { val position = viewHolder.absoluteAdapterPosition - 1 lifecycleScope.launch { - SongRepository.removeSongFromPlaylist(playlistId, position) + songRepository.removeSongFromPlaylist(playlistId, position) } } }) @@ -150,7 +151,7 @@ class PlaylistSongsFragment : RecyclerViewFragment() } lifecycleScope.launch { - playlist = SongRepository.getPlaylistById(playlistId) + playlist = songRepository.getPlaylistById(playlistId) requireAppCompatActivity().title = playlist.playlist.name songsViewModel.getPlaylistSongsAsFlow(playlistId).collectLatest { adapter.submitList(it, animation = false) diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/youtube/YouTubeSuggestionFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/youtube/YouTubeSuggestionFragment.kt index 61a29a151..d6d3ced52 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/youtube/YouTubeSuggestionFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/youtube/YouTubeSuggestionFragment.kt @@ -132,7 +132,7 @@ class YouTubeSuggestionFragment : NavigationFragment(fragment), IAlbumMenuListener { + private val songRepository by lazy { SongRepository(fragment.requireContext()) } + override suspend fun getMediaMetadata(items: List): List = items.flatMap { album -> - SongRepository.getAlbumSongs(album.id) + songRepository.getAlbumSongs(album.id) }.map { it.toMediaMetadata() } @@ -47,7 +49,7 @@ class AlbumMenuListener(override val fragment: Fragment) : BaseMenuListener) { addToPlaylist { playlist -> - SongRepository.addToPlaylist(playlist, albums) + songRepository.addToPlaylist(playlist, albums) } } @@ -69,14 +71,14 @@ class AlbumMenuListener(override val fragment: Fragment) : BaseMenuListener) { GlobalScope.launch(context.exceptionHandler) { - SongRepository.refetchAlbums(albums.map { it.album }) + songRepository.refetchAlbums(albums.map { it.album }) } } @OptIn(DelicateCoroutinesApi::class) override fun delete(albums: List) { GlobalScope.launch(context.exceptionHandler) { - SongRepository.deleteAlbums(albums) + songRepository.deleteAlbums(albums) } } } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/ui/listeners/ArtistMenuListener.kt b/app/src/main/java/com/zionhuang/music/ui/listeners/ArtistMenuListener.kt index c32c67447..0f6d113ca 100644 --- a/app/src/main/java/com/zionhuang/music/ui/listeners/ArtistMenuListener.kt +++ b/app/src/main/java/com/zionhuang/music/ui/listeners/ArtistMenuListener.kt @@ -34,8 +34,10 @@ interface IArtistMenuListener { } class ArtistMenuListener(override val fragment: Fragment) : BaseMenuListener(fragment), IArtistMenuListener { + private val songRepository by lazy { SongRepository(fragment.requireContext()) } + override suspend fun getMediaMetadata(items: List): List = items.flatMap { artist -> - SongRepository.getArtistSongs(artist.id, SongSortInfoPreference).getList() + songRepository.getArtistSongs(artist.id, SongSortInfoPreference).getList() }.map { it.toMediaMetadata() } @@ -56,7 +58,7 @@ class ArtistMenuListener(override val fragment: Fragment) : BaseMenuListener) { addToPlaylist { playlist -> - SongRepository.addToPlaylist(playlist, artists) + songRepository.addToPlaylist(playlist, artists) } } @@ -74,7 +76,7 @@ class ArtistMenuListener(override val fragment: Fragment) : BaseMenuListener) { GlobalScope.launch(context.exceptionHandler) { - SongRepository.refetchArtists(artists.map { it.artist }) + songRepository.refetchArtists(artists.map { it.artist }) } } } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/ui/listeners/CustomPlaylistMenuListener.kt b/app/src/main/java/com/zionhuang/music/ui/listeners/CustomPlaylistMenuListener.kt index 0475046e7..859a05e19 100644 --- a/app/src/main/java/com/zionhuang/music/ui/listeners/CustomPlaylistMenuListener.kt +++ b/app/src/main/java/com/zionhuang/music/ui/listeners/CustomPlaylistMenuListener.kt @@ -14,8 +14,10 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch class LikedPlaylistMenuListener(override val fragment: Fragment) : BaseMenuListener(fragment) { + private val songRepository by lazy { SongRepository(fragment.requireContext()) } + override suspend fun getMediaMetadata(items: List): List = - SongRepository.getLikedSongs(SongSortInfoPreference).getList().map { it.toMediaMetadata() } + songRepository.getLikedSongs(SongSortInfoPreference).getList().map { it.toMediaMetadata() } fun play() { playAll(context.getString(R.string.liked_songs), emptyList()) @@ -31,21 +33,23 @@ class LikedPlaylistMenuListener(override val fragment: Fragment) : BaseMenuListe fun addToPlaylist() { addToPlaylist { playlist -> - SongRepository.addToPlaylist(playlist, SongRepository.getLikedSongs(SongSortInfoPreference).getList()) + songRepository.addToPlaylist(playlist, songRepository.getLikedSongs(SongSortInfoPreference).getList()) } } @OptIn(DelicateCoroutinesApi::class) fun download() { GlobalScope.launch(context.exceptionHandler) { - SongRepository.downloadSongs(SongRepository.getLikedSongs(SongSortInfoPreference).getList().map { it.song }) + songRepository.downloadSongs(songRepository.getLikedSongs(SongSortInfoPreference).getList().map { it.song }) } } } class DownloadedPlaylistMenuListener(override val fragment: Fragment) : BaseMenuListener(fragment) { + private val songRepository by lazy { SongRepository(fragment.requireContext()) } + override suspend fun getMediaMetadata(items: List): List = - SongRepository.getDownloadedSongs(SongSortInfoPreference).getList().map { it.toMediaMetadata() } + songRepository.getDownloadedSongs(SongSortInfoPreference).getList().map { it.toMediaMetadata() } fun play() { playAll(context.getString(R.string.downloaded_songs), emptyList()) @@ -61,7 +65,7 @@ class DownloadedPlaylistMenuListener(override val fragment: Fragment) : BaseMenu fun addToPlaylist() { addToPlaylist { playlist -> - SongRepository.addToPlaylist(playlist, SongRepository.getLikedSongs(SongSortInfoPreference).getList()) + songRepository.addToPlaylist(playlist, songRepository.getLikedSongs(SongSortInfoPreference).getList()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/ui/listeners/PlaylistMenuListener.kt b/app/src/main/java/com/zionhuang/music/ui/listeners/PlaylistMenuListener.kt index aa30e4074..bf6d771a9 100644 --- a/app/src/main/java/com/zionhuang/music/ui/listeners/PlaylistMenuListener.kt +++ b/app/src/main/java/com/zionhuang/music/ui/listeners/PlaylistMenuListener.kt @@ -44,12 +44,14 @@ interface IPlaylistMenuListener { } class PlaylistMenuListener(override val fragment: Fragment) : BaseMenuListener(fragment), IPlaylistMenuListener { + private val songRepository by lazy { SongRepository(fragment.requireContext()) } + override suspend fun getMediaMetadata(items: List): List = withContext(IO) { items.flatMap { playlist -> if (playlist.playlist.isYouTubePlaylist) { YouTube.getQueue(playlistId = playlist.id).getOrThrow().map { it.toMediaMetadata() } } else { - SongRepository.getPlaylistSongs(playlist.id).getList().map { it.toMediaMetadata() } + songRepository.getPlaylistSongs(playlist.id).getList().map { it.toMediaMetadata() } } } } @@ -78,14 +80,14 @@ class PlaylistMenuListener(override val fragment: Fragment) : BaseMenuListener

) { addToPlaylist { playlist -> - SongRepository.addToPlaylist(playlist, playlists) + songRepository.addToPlaylist(playlist, playlists) } } @OptIn(DelicateCoroutinesApi::class) override fun download(playlists: List) { GlobalScope.launch(context.exceptionHandler) { - SongRepository.downloadPlaylists(playlists) + songRepository.downloadPlaylists(playlists) } } @@ -103,14 +105,14 @@ class PlaylistMenuListener(override val fragment: Fragment) : BaseMenuListener

) { GlobalScope.launch(context.exceptionHandler) { - SongRepository.refetchPlaylists(playlists) + songRepository.refetchPlaylists(playlists) } } @OptIn(DelicateCoroutinesApi::class) override fun delete(playlists: List) { GlobalScope.launch(context.exceptionHandler) { - SongRepository.deletePlaylists(playlists.map { it.playlist }) + songRepository.deletePlaylists(playlists.map { it.playlist }) } } } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/ui/listeners/SongMenuListener.kt b/app/src/main/java/com/zionhuang/music/ui/listeners/SongMenuListener.kt index c8e7238e5..c408496c9 100644 --- a/app/src/main/java/com/zionhuang/music/ui/listeners/SongMenuListener.kt +++ b/app/src/main/java/com/zionhuang/music/ui/listeners/SongMenuListener.kt @@ -52,6 +52,8 @@ interface ISongMenuListener { } class SongMenuListener(override val fragment: Fragment) : BaseMenuListener(fragment), ISongMenuListener { + private val songRepository by lazy { SongRepository(fragment.requireContext()) } + override suspend fun getMediaMetadata(items: List): List = items.map { it.toMediaMetadata() } override fun editSong(song: Song) { @@ -63,7 +65,7 @@ class SongMenuListener(override val fragment: Fragment) : BaseMenuListener @OptIn(DelicateCoroutinesApi::class) override fun toggleLike(song: Song) { GlobalScope.launch(context.exceptionHandler) { - SongRepository.toggleLiked(song) + songRepository.toggleLiked(song) } } @@ -81,7 +83,7 @@ class SongMenuListener(override val fragment: Fragment) : BaseMenuListener override fun addToPlaylist(songs: List) { addToPlaylist { playlist -> - SongRepository.addToPlaylist(playlist, songs) + songRepository.addToPlaylist(playlist, songs) } } @@ -89,7 +91,7 @@ class SongMenuListener(override val fragment: Fragment) : BaseMenuListener override fun download(songs: List) { GlobalScope.launch(context.exceptionHandler) { Snackbar.make(mainActivity.binding.mainContent, context.resources.getQuantityString(R.plurals.snackbar_download_song, songs.size, songs.size), LENGTH_SHORT).show() - SongRepository.downloadSongs(songs.map { it.song }) + songRepository.downloadSongs(songs.map { it.song }) } } @@ -97,7 +99,7 @@ class SongMenuListener(override val fragment: Fragment) : BaseMenuListener override fun removeDownload(songs: List) { val mainContent = mainActivity.binding.mainContent GlobalScope.launch { - SongRepository.removeDownloads(songs) + songRepository.removeDownloads(songs) Snackbar.make(mainContent, R.string.snackbar_removed_download, LENGTH_SHORT).show() } } @@ -122,7 +124,7 @@ class SongMenuListener(override val fragment: Fragment) : BaseMenuListener @OptIn(DelicateCoroutinesApi::class) override fun refetch(songs: List) { GlobalScope.launch(context.exceptionHandler) { - SongRepository.refetchSongs(songs) + songRepository.refetchSongs(songs) } } @@ -138,7 +140,7 @@ class SongMenuListener(override val fragment: Fragment) : BaseMenuListener @OptIn(DelicateCoroutinesApi::class) override fun delete(songs: List) { GlobalScope.launch { - SongRepository.deleteSongs(songs) + songRepository.deleteSongs(songs) } } } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/ui/listeners/YTItemBatchMenuListener.kt b/app/src/main/java/com/zionhuang/music/ui/listeners/YTItemBatchMenuListener.kt index f923efb21..35268d714 100644 --- a/app/src/main/java/com/zionhuang/music/ui/listeners/YTItemBatchMenuListener.kt +++ b/app/src/main/java/com/zionhuang/music/ui/listeners/YTItemBatchMenuListener.kt @@ -28,6 +28,7 @@ interface IYTItemBatchMenuListener { } class YTItemBatchMenuListener(val fragment: Fragment) : IYTItemBatchMenuListener { + private val songRepository by lazy { SongRepository(fragment.requireContext()) } val context: Context get() = fragment.requireContext() @@ -98,9 +99,9 @@ class YTItemBatchMenuListener(val fragment: Fragment) : IYTItemBatchMenuListener override fun addToLibrary(items: List) { val mainContent = mainActivity.binding.mainContent GlobalScope.launch(context.exceptionHandler) { - SongRepository.safeAddSongs(items.filterIsInstance()) - SongRepository.addAlbums(items.filterIsInstance()) - SongRepository.addPlaylists(items.filterIsInstance()) + songRepository.safeAddSongs(items.filterIsInstance()) + songRepository.addAlbums(items.filterIsInstance()) + songRepository.addPlaylists(items.filterIsInstance()) Snackbar.make(mainContent, R.string.snackbar_added_to_library, LENGTH_SHORT).show() } } @@ -110,7 +111,7 @@ class YTItemBatchMenuListener(val fragment: Fragment) : IYTItemBatchMenuListener val mainContent = mainActivity.binding.mainContent ChoosePlaylistDialog { playlist -> GlobalScope.launch(context.exceptionHandler) { - SongRepository.addYouTubeItemsToPlaylist(playlist, items) + songRepository.addYouTubeItemsToPlaylist(playlist, items) Snackbar.make(mainContent, fragment.getString(R.string.snackbar_added_to_playlist, playlist.name), LENGTH_SHORT) .setAction(R.string.snackbar_action_view) { fragment.exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true).addTarget(R.id.fragment_content) diff --git a/app/src/main/java/com/zionhuang/music/ui/viewholders/LocalItemViewHolder.kt b/app/src/main/java/com/zionhuang/music/ui/viewholders/LocalItemViewHolder.kt index 080932311..c48ec8845 100644 --- a/app/src/main/java/com/zionhuang/music/ui/viewholders/LocalItemViewHolder.kt +++ b/app/src/main/java/com/zionhuang/music/ui/viewholders/LocalItemViewHolder.kt @@ -120,7 +120,7 @@ class ArtistViewHolder( binding.selectedIndicator.isVisible = isSelected if (artist.artist.bannerUrl == null || Duration.between(artist.artist.lastUpdateTime, LocalDateTime.now()) > Duration.ofDays(10)) { GlobalScope.launch(binding.context.exceptionHandler) { - SongRepository.refetchArtist(artist.artist) + SongRepository(binding.context).refetchArtist(artist.artist) } } } @@ -164,7 +164,7 @@ class AlbumViewHolder( binding.selectedIndicator.isVisible = isSelected if (album.album.thumbnailUrl == null || album.album.year == null) { GlobalScope.launch(binding.context.exceptionHandler) { - SongRepository.refetchAlbum(album.album) + SongRepository(binding.context).refetchAlbum(album.album) } } } diff --git a/app/src/main/java/com/zionhuang/music/ui/viewholders/YouTubeViewHolder.kt b/app/src/main/java/com/zionhuang/music/ui/viewholders/YouTubeViewHolder.kt index acd8bed4f..48d666245 100644 --- a/app/src/main/java/com/zionhuang/music/ui/viewholders/YouTubeViewHolder.kt +++ b/app/src/main/java/com/zionhuang/music/ui/viewholders/YouTubeViewHolder.kt @@ -244,7 +244,7 @@ class YouTubeSuggestionViewHolder( if (item.source == LOCAL) { binding.deleteBtn.setOnClickListener { GlobalScope.launch { - SongRepository.deleteSearchHistory(item.query) + SongRepository(binding.context).deleteSearchHistory(item.query) onRefreshSuggestions() } } diff --git a/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt b/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt index 1c8ea257a..7ffd00a35 100644 --- a/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt +++ b/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt @@ -11,7 +11,6 @@ import android.text.StaticLayout import android.text.TextPaint import android.text.format.DateUtils.SECOND_IN_MILLIS import android.util.AttributeSet -import android.util.Log import android.view.GestureDetector import android.view.GestureDetector.SimpleOnGestureListener import android.view.MotionEvent @@ -258,7 +257,6 @@ class LyricsView @JvmOverloads constructor( fun hasLyrics(): Boolean = lrcEntryList.isNotEmpty() fun updateTime(time: Long, animate: Boolean = true) { - Log.d("DBG", "update time: $time, animate: $animate") runOnUi { if (!hasLyrics() || !isSyncedLyrics) { return@runOnUi diff --git a/app/src/main/java/com/zionhuang/music/utils/NavigationEndpointHandler.kt b/app/src/main/java/com/zionhuang/music/utils/NavigationEndpointHandler.kt index 743f1a234..4bc6c958c 100644 --- a/app/src/main/java/com/zionhuang/music/utils/NavigationEndpointHandler.kt +++ b/app/src/main/java/com/zionhuang/music/utils/NavigationEndpointHandler.kt @@ -30,6 +30,7 @@ open class NavigationEndpointHandler(val fragment: Fragment) { get() = fragment.requireActivity() as MainActivity val context: Context get() = fragment.requireContext() + private val songRepository by lazy { SongRepository(fragment.requireContext()) } fun handle(navigationEndpoint: NavigationEndpoint?, item: YTItem? = null) = navigationEndpoint?.endpoint?.let { handle(it, item) } @@ -92,9 +93,9 @@ open class NavigationEndpointHandler(val fragment: Fragment) { val mainContent = mainActivity.binding.mainContent GlobalScope.launch(context.exceptionHandler) { when (item) { - is SongItem -> SongRepository.safeAddSong(item) - is AlbumItem -> SongRepository.addAlbum(item) - is PlaylistItem -> SongRepository.addPlaylist(item) + is SongItem -> songRepository.safeAddSong(item) + is AlbumItem -> songRepository.addAlbum(item) + is PlaylistItem -> songRepository.addPlaylist(item) else -> {} } Snackbar.make(mainContent, R.string.snackbar_added_to_library, LENGTH_SHORT).show() @@ -104,7 +105,7 @@ open class NavigationEndpointHandler(val fragment: Fragment) { fun importPlaylist(playlist: PlaylistItem) { val mainContent = mainActivity.binding.mainContent GlobalScope.launch(context.exceptionHandler) { - SongRepository.importPlaylist(playlist) + songRepository.importPlaylist(playlist) Snackbar.make(mainContent, R.string.snackbar_playlist_imported, LENGTH_SHORT).show() } } @@ -113,7 +114,7 @@ open class NavigationEndpointHandler(val fragment: Fragment) { val mainContent = mainActivity.binding.mainContent ChoosePlaylistDialog { playlist -> GlobalScope.launch(context.exceptionHandler) { - SongRepository.addYouTubeItemToPlaylist(playlist, item) + songRepository.addYouTubeItemToPlaylist(playlist, item) Snackbar.make(mainContent, fragment.getString(R.string.snackbar_added_to_playlist, playlist.name), LENGTH_SHORT) .setAction(R.string.snackbar_action_view) { fragment.exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true).addTarget(R.id.fragment_content) diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/LocalSearchViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/LocalSearchViewModel.kt index 1f75af17d..bd1b0f4fd 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/LocalSearchViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/LocalSearchViewModel.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.flatMapLatest @OptIn(ExperimentalCoroutinesApi::class) class LocalSearchViewModel(application: Application) : AndroidViewModel(application) { + private val songRepository = SongRepository(application) val query = SafeMutableLiveData("") val filter = SafeMutableLiveData(Filter.ALL) val result: Flow> = query.asFlow().combine(filter.asFlow()) { query: String, filter: Filter -> @@ -23,11 +24,11 @@ class LocalSearchViewModel(application: Application) : AndroidViewModel(applicat emptyFlow() } else { when (filter) { - Filter.ALL -> SongRepository.searchAll(query) - Filter.SONG -> SongRepository.searchSongs(query) - Filter.ALBUM -> SongRepository.searchAlbums(query) - Filter.ARTIST -> SongRepository.searchArtists(query) - Filter.PLAYLIST -> SongRepository.searchPlaylists(query) + Filter.ALL -> songRepository.searchAll(query) + Filter.SONG -> songRepository.searchSongs(query) + Filter.ALBUM -> songRepository.searchAlbums(query) + Filter.ARTIST -> songRepository.searchArtists(query) + Filter.PLAYLIST -> songRepository.searchPlaylists(query) } } } diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/PlaybackViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/PlaybackViewModel.kt index aa7f07d26..d5fb33561 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/PlaybackViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/PlaybackViewModel.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalCoroutinesApi::class) class PlaybackViewModel(application: Application) : AndroidViewModel(application) { + private val songRepository = SongRepository(application) val transportControls: TransportControls? get() = MediaSessionConnection.transportControls val mediaMetadata = MediaSessionConnection.mediaMetadata @@ -38,13 +39,13 @@ class PlaybackViewModel(application: Application) : AndroidViewModel(application MediaSessionConnection.binder?.songPlayer?.playerVolume ?: emptyFlow() } val currentSong = mediaMetadata.flatMapLatest { mediaMetadata -> - SongRepository.getSongById(mediaMetadata?.getString(METADATA_KEY_MEDIA_ID)).flow + songRepository.getSongById(mediaMetadata?.getString(METADATA_KEY_MEDIA_ID)).flow } val currentSongFormat = mediaMetadata.flatMapLatest { mediaMetadata -> - SongRepository.getSongFormat(mediaMetadata?.getString(METADATA_KEY_MEDIA_ID)).getFlow() + songRepository.getSongFormat(mediaMetadata?.getString(METADATA_KEY_MEDIA_ID)).getFlow() } val currentLyrics = mediaMetadata.flatMapLatest { mediaMetadata -> - SongRepository.getLyrics(mediaMetadata?.getString(METADATA_KEY_MEDIA_ID)) + songRepository.getLyrics(mediaMetadata?.getString(METADATA_KEY_MEDIA_ID)) }.stateIn(viewModelScope, SharingStarted.Lazily, null) val showLyrics = preferenceLiveData(R.string.pref_show_lyrics, false) diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/SongsViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/SongsViewModel.kt index 3eb47afce..61dcfa65c 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/SongsViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/SongsViewModel.kt @@ -15,29 +15,31 @@ import kotlinx.coroutines.flow.map @OptIn(ExperimentalCoroutinesApi::class) class SongsViewModel(application: Application) : AndroidViewModel(application) { + private val songRepository = SongRepository(application) + val allSongsFlow = SongSortInfoPreference.flow.flatMapLatest { sortInfo -> - SongRepository.getAllSongs(sortInfo).flow + songRepository.getAllSongs(sortInfo).flow }.map { list -> - listOf(SongHeader(SongRepository.getSongCount(), SongSortInfoPreference.currentInfo)) + list + listOf(SongHeader(songRepository.getSongCount(), SongSortInfoPreference.currentInfo)) + list } val allArtistsFlow = ArtistSortInfoPreference.flow.flatMapLatest { sortInfo -> - SongRepository.getAllArtists(sortInfo).flow + songRepository.getAllArtists(sortInfo).flow }.map { list -> - listOf(ArtistHeader(SongRepository.getArtistCount(), ArtistSortInfoPreference.currentInfo)) + list + listOf(ArtistHeader(songRepository.getArtistCount(), ArtistSortInfoPreference.currentInfo)) + list } val allAlbumsFlow = AlbumSortInfoPreference.flow.flatMapLatest { sortInfo -> - SongRepository.getAllAlbums(sortInfo).flow + songRepository.getAllAlbums(sortInfo).flow }.map { list -> - listOf(AlbumHeader(SongRepository.getAlbumCount(), AlbumSortInfoPreference.currentInfo)) + list + listOf(AlbumHeader(songRepository.getAlbumCount(), AlbumSortInfoPreference.currentInfo)) + list } val allPlaylistsFlow = combine( - SongRepository.getLikedSongCount().map { LikedPlaylist(it) }, - SongRepository.getDownloadedSongCount().map { DownloadedPlaylist(it) }, + songRepository.getLikedSongCount().map { LikedPlaylist(it) }, + songRepository.getDownloadedSongCount().map { DownloadedPlaylist(it) }, PlaylistSortInfoPreference.flow.flatMapLatest { sortInfo -> - SongRepository.getAllPlaylists(sortInfo).flow + songRepository.getAllPlaylists(sortInfo).flow } ) { likedPlaylist, downloadedPlaylist, playlists -> listOf( @@ -48,23 +50,23 @@ class SongsViewModel(application: Application) : AndroidViewModel(application) { } fun getArtistSongsAsFlow(artistId: String) = SongSortInfoPreference.flow.flatMapLatest { sortInfo -> - SongRepository.getArtistSongs(artistId, sortInfo).flow + songRepository.getArtistSongs(artistId, sortInfo).flow }.map { list -> - listOf(SongHeader(SongRepository.getArtistSongCount(artistId), SongSortInfoPreference.currentInfo)) + list + listOf(SongHeader(songRepository.getArtistSongCount(artistId), SongSortInfoPreference.currentInfo)) + list } - fun getPlaylistSongsAsFlow(playlistId: String) = SongRepository.getPlaylistSongs(playlistId).flow.map { list -> + fun getPlaylistSongsAsFlow(playlistId: String) = songRepository.getPlaylistSongs(playlistId).flow.map { list -> listOf(PlaylistSongHeader(list.size, list.sumOf { it.song.duration.toLong() })) + list } fun getLikedSongsAsFlow() = SongSortInfoPreference.flow.flatMapLatest { sortInfo -> - SongRepository.getLikedSongs(sortInfo).flow + songRepository.getLikedSongs(sortInfo).flow }.map { list -> listOf(SongHeader(list.size, SongSortInfoPreference.currentInfo)) + list } fun getDownloadedSongsAsFlow() = SongSortInfoPreference.flow.flatMapLatest { sortInfo -> - SongRepository.getDownloadedSongs(sortInfo).flow + songRepository.getDownloadedSongs(sortInfo).flow }.map { list -> listOf(SongHeader(list.size, SongSortInfoPreference.currentInfo)) + list } diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/SuggestionViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/SuggestionViewModel.kt index 24cb27a4e..d6adc4deb 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/SuggestionViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/SuggestionViewModel.kt @@ -12,19 +12,21 @@ import com.zionhuang.music.repos.YouTubeRepository import kotlinx.coroutines.launch class SuggestionViewModel(application: Application) : AndroidViewModel(application) { + private val songRepository = SongRepository(application) + private val youTubeRepository = YouTubeRepository(application) val suggestions = MutableLiveData>(emptyList()) fun fetchSuggestions(query: String?) = viewModelScope.launch { if (query.isNullOrEmpty()) { - suggestions.postValue(SongRepository.getAllSearchHistory().map { SuggestionTextItem(it.query, LOCAL) }) + suggestions.postValue(songRepository.getAllSearchHistory().map { SuggestionTextItem(it.query, LOCAL) }) } else { try { - val history = SongRepository.getSearchHistory(query).map { SuggestionTextItem(it.query, LOCAL) } - suggestions.postValue(history + YouTubeRepository.getSuggestions(query).filter { item -> + val history = songRepository.getSearchHistory(query).map { SuggestionTextItem(it.query, LOCAL) } + suggestions.postValue(history + youTubeRepository.getSuggestions(query).filter { item -> item !is SuggestionTextItem || history.find { it.query == item.query } == null }) } catch (e: Exception) { - // TODO + e.printStackTrace() } } } diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeBrowseViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeBrowseViewModel.kt index d7c6454bd..b181c9e21 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeBrowseViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeBrowseViewModel.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.withContext class YouTubeBrowseViewModel(application: Application, private val browseEndpoint: BrowseEndpoint) : AndroidViewModel(application) { + private val youTubeRepository = YouTubeRepository(application) private var _albumName: String? = null val albumName: String? get() = _albumName private var _albumSongs: List? = null @@ -43,7 +44,7 @@ class YouTubeBrowseViewModel(application: Application, private val browseEndpoin override fun getRefreshKey(state: PagingState, YTBaseItem>): List? = null } } else { - YouTubeRepository.browse(browseEndpoint) + youTubeRepository.browse(browseEndpoint) } }.flow.cachedIn(viewModelScope) } diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeSearchViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeSearchViewModel.kt index 68fb6fb95..af8e69c16 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeSearchViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/YouTubeSearchViewModel.kt @@ -10,12 +10,13 @@ import com.zionhuang.innertube.YouTube import com.zionhuang.music.repos.YouTubeRepository class YouTubeSearchViewModel(application: Application, query: String) : AndroidViewModel(application) { + private val youTubeRepository = YouTubeRepository(application) val filter = MutableLiveData(null) val pagingData = Pager(PagingConfig(pageSize = 20)) { filter.value.let { - if (it == null) YouTubeRepository.searchAll(query) - else YouTubeRepository.search(query, it) + if (it == null) youTubeRepository.searchAll(query) + else youTubeRepository.search(query, it) } }.flow.cachedIn(viewModelScope) } From 707e389e68f729f771523c97f268129d34d78cbf Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Thu, 27 Oct 2022 23:24:34 +0800 Subject: [PATCH 47/95] Add document provider (#117) --- app/src/main/AndroidManifest.xml | 11 ++ .../zionhuang/music/provider/SongsProvider.kt | 133 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 app/src/main/java/com/zionhuang/music/provider/SongsProvider.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3d48b5ba7..6cfac45b0 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -135,6 +135,17 @@ + + + + + + ?): Cursor = + MatrixCursor(projection ?: ROOT_PROJECTION).apply { + newRow() + .add(Root.COLUMN_ROOT_ID, ROOT) + .add(Root.COLUMN_DOCUMENT_ID, ROOT_DOC) + .add(Root.COLUMN_TITLE, context!!.getString(R.string.app_name)) + .add(Root.COLUMN_ICON, R.drawable.ic_launcher_foreground) + .add(Root.COLUMN_MIME_TYPES, "*/*") + .add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_SEARCH) + } + + override fun queryDocument(documentId: String, projection: Array?): Cursor = runBlocking { + val cursor = MatrixCursor(projection ?: DOCUMENT_PROJECTION) + when (documentId) { + ROOT_DOC -> cursor.newRow() + .add(Document.COLUMN_DOCUMENT_ID, documentId) + .add(Document.COLUMN_DISPLAY_NAME, "Root") + .add(Document.COLUMN_MIME_TYPE, MIME_TYPE_DIR) +// .add(Document.COLUMN_FLAGS, FLAG_SUPPORTS_THUMBNAIL) + else -> { + val song = songRepository.getSongById(documentId).getValueAsync() ?: throw FileNotFoundException() + val format = songRepository.getSongFormat(documentId).getValueAsync() ?: throw FileNotFoundException() + cursor.newRow() + .add(Document.COLUMN_DOCUMENT_ID, documentId) + .add(Document.COLUMN_DISPLAY_NAME, song.song.title) + .add(Document.COLUMN_MIME_TYPE, format.mimeType) + .add(Document.COLUMN_SIZE, format.contentLength) + .add(Document.COLUMN_LAST_MODIFIED, song.song.modifyDate.atZone(ZoneOffset.UTC).toInstant().toEpochMilli()) +// .add(Document.COLUMN_FLAGS, FLAG_SUPPORTS_THUMBNAIL) + } + } + cursor + } + + override fun queryChildDocuments(parentDocumentId: String, projection: Array?, sortOrder: String?): Cursor = runBlocking { + val result = MatrixCursor(DOCUMENT_PROJECTION) + when (parentDocumentId) { + ROOT_DOC -> result.apply { + songRepository.getDownloadedSongs(SongSortInfoPreference).flow.first().forEach { song -> + val format = songRepository.getSongFormat(song.id).getValueAsync() + if (format != null) { + newRow() + .add(Document.COLUMN_DOCUMENT_ID, song.id) + .add(Document.COLUMN_DISPLAY_NAME, "${song.song.title}${mimeToExt(format.mimeType)}") + .add(Document.COLUMN_MIME_TYPE, format.mimeType) + .add(Document.COLUMN_SIZE, format.contentLength) + .add(Document.COLUMN_LAST_MODIFIED, song.song.modifyDate.atZone(ZoneOffset.UTC).toInstant().toEpochMilli()) +// .add(Document.COLUMN_FLAGS, FLAG_SUPPORTS_THUMBNAIL) + } + } + } + else -> {} + } + result + } + + override fun openDocument(documentId: String, mode: String?, signal: CancellationSignal?): ParcelFileDescriptor = runBlocking { + val file = songRepository.getSongFile(documentId) + ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode)) + } + + private fun mimeToExt(mimeType: String) = when (FileTypes.inferFileTypeFromMimeType(mimeType)) { + FileTypes.AC3 -> ".ac3" + FileTypes.AC4 -> ".ac4" + FileTypes.ADTS -> ".adts" + FileTypes.AMR -> ".amr" + FileTypes.AVI -> ".avi" + FileTypes.FLAC -> ".flac" + FileTypes.FLV -> ".flv" + FileTypes.JPEG -> ".jpg" + FileTypes.MATROSKA -> ".webm" + FileTypes.MIDI -> ".midi" + FileTypes.MP3 -> ".mp3" + FileTypes.MP4 -> ".m4a" + FileTypes.OGG -> ".ogg" + FileTypes.PS -> ".ps" + FileTypes.TS -> ".ts" + FileTypes.WAV -> ".wav" + FileTypes.WEBVTT -> ".webvtt" + else -> "" + } + + companion object { + private const val ROOT = "root" + private const val ROOT_DOC = "root_dir" + + private val ROOT_PROJECTION: Array = arrayOf( + Root.COLUMN_ROOT_ID, + Root.COLUMN_DOCUMENT_ID, + Root.COLUMN_TITLE, + Root.COLUMN_SUMMARY, + Root.COLUMN_ICON, + Root.COLUMN_MIME_TYPES, + Root.COLUMN_FLAGS, + Root.COLUMN_AVAILABLE_BYTES + ) + private val DOCUMENT_PROJECTION: Array = arrayOf( + Document.COLUMN_DOCUMENT_ID, + Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_MIME_TYPE, + Document.COLUMN_SIZE, + Document.COLUMN_LAST_MODIFIED, + Document.COLUMN_FLAGS + ) + } +} \ No newline at end of file From cbded30f7595f588298f3c28692883d3c0a39091 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Thu, 27 Oct 2022 23:45:56 +0800 Subject: [PATCH 48/95] Improve song cache summary --- .../music/ui/fragments/settings/CacheSettingsFragment.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/settings/CacheSettingsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/settings/CacheSettingsFragment.kt index 22cb2dbc4..c54d79dac 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/settings/CacheSettingsFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/settings/CacheSettingsFragment.kt @@ -23,7 +23,9 @@ class CacheSettingsFragment : BaseSettingsFragment() { if (iBinder !is MusicService.MusicBinder) return findPreference(getString(R.string.pref_song_max_cache_size))?.apply { MediaSessionConnection.binder?.cache?.let { cache -> - summary = getString(R.string.size_used, Formatter.formatShortFileSize(context, tryOrNull { cache.cacheSpace } ?: 0)) + tryOrNull { cache.cacheSpace }?.let { used -> + summary = getString(R.string.size_used, Formatter.formatShortFileSize(context, used)) + } } } } From 6c8539dd4a822262d2fe427065a37cd35e5b95ce Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Fri, 28 Oct 2022 13:21:58 +0800 Subject: [PATCH 49/95] Add link to open SAF --- app/src/main/java/com/zionhuang/music/App.kt | 2 +- .../zionhuang/music/playback/SongPlayer.kt | 2 +- .../zionhuang/music/provider/SongsProvider.kt | 36 +++++++++---------- .../music/ui/fragments/SettingsFragment.kt | 4 +-- ...Fragment.kt => StorageSettingsFragment.kt} | 17 +++++++-- app/src/main/res/drawable/ic_folder.xml | 10 ++++++ .../navigation/settings_navigation_graph.xml | 6 ++-- app/src/main/res/values-es-rUS/strings.xml | 2 ++ app/src/main/res/values-es/strings.xml | 2 ++ app/src/main/res/values-fa-rIR/strings.xml | 2 ++ app/src/main/res/values-fi-rFI/strings.xml | 2 ++ app/src/main/res/values-fr-rFR/strings.xml | 2 ++ app/src/main/res/values-hu/strings.xml | 2 ++ app/src/main/res/values-it/strings.xml | 2 ++ app/src/main/res/values-ja-rJP/strings.xml | 2 ++ app/src/main/res/values-ko-rKR/strings.xml | 2 ++ app/src/main/res/values-ml-rIN/strings.xml | 2 ++ app/src/main/res/values-pt-rBR/strings.xml | 2 ++ app/src/main/res/values-sv-rSE/strings.xml | 2 ++ app/src/main/res/values-zh-rCN/strings.xml | 2 ++ app/src/main/res/values-zh-rTW/strings.xml | 4 ++- app/src/main/res/values/constants.xml | 3 +- app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/xml/pref_cache.xml | 23 ------------ app/src/main/res/xml/pref_main.xml | 4 +-- app/src/main/res/xml/pref_storage.xml | 31 ++++++++++++++++ 26 files changed, 115 insertions(+), 55 deletions(-) rename app/src/main/java/com/zionhuang/music/ui/fragments/settings/{CacheSettingsFragment.kt => StorageSettingsFragment.kt} (80%) create mode 100644 app/src/main/res/drawable/ic_folder.xml delete mode 100644 app/src/main/res/xml/pref_cache.xml create mode 100644 app/src/main/res/xml/pref_storage.xml diff --git a/app/src/main/java/com/zionhuang/music/App.kt b/app/src/main/java/com/zionhuang/music/App.kt index 5e5bbe99f..90bb72cac 100644 --- a/app/src/main/java/com/zionhuang/music/App.kt +++ b/app/src/main/java/com/zionhuang/music/App.kt @@ -16,7 +16,7 @@ import com.zionhuang.kugou.KuGou import com.zionhuang.music.extensions.getEnum import com.zionhuang.music.extensions.sharedPreferences import com.zionhuang.music.extensions.toInetSocketAddress -import com.zionhuang.music.ui.fragments.settings.CacheSettingsFragment.Companion.VALUE_TO_MB +import com.zionhuang.music.ui.fragments.settings.StorageSettingsFragment.Companion.VALUE_TO_MB import java.net.Proxy import java.util.* diff --git a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt index a93588a76..7932b0f3c 100644 --- a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt +++ b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt @@ -87,7 +87,7 @@ import com.zionhuang.music.playback.queues.YouTubeQueue import com.zionhuang.music.repos.SongRepository import com.zionhuang.music.ui.activities.MainActivity import com.zionhuang.music.ui.bindings.resizeThumbnailUrl -import com.zionhuang.music.ui.fragments.settings.CacheSettingsFragment.Companion.VALUE_TO_MB +import com.zionhuang.music.ui.fragments.settings.StorageSettingsFragment.Companion.VALUE_TO_MB import com.zionhuang.music.utils.InfoCache import com.zionhuang.music.utils.preference.enumPreference import kotlinx.coroutines.* diff --git a/app/src/main/java/com/zionhuang/music/provider/SongsProvider.kt b/app/src/main/java/com/zionhuang/music/provider/SongsProvider.kt index 6e5d5e1d1..f60877aa3 100644 --- a/app/src/main/java/com/zionhuang/music/provider/SongsProvider.kt +++ b/app/src/main/java/com/zionhuang/music/provider/SongsProvider.kt @@ -26,7 +26,7 @@ class SongsProvider : DocumentsProvider() { } override fun queryRoots(projection: Array?): Cursor = - MatrixCursor(projection ?: ROOT_PROJECTION).apply { + MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION).apply { newRow() .add(Root.COLUMN_ROOT_ID, ROOT) .add(Root.COLUMN_DOCUMENT_ID, ROOT_DOC) @@ -37,7 +37,7 @@ class SongsProvider : DocumentsProvider() { } override fun queryDocument(documentId: String, projection: Array?): Cursor = runBlocking { - val cursor = MatrixCursor(projection ?: DOCUMENT_PROJECTION) + val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION) when (documentId) { ROOT_DOC -> cursor.newRow() .add(Document.COLUMN_DOCUMENT_ID, documentId) @@ -60,25 +60,23 @@ class SongsProvider : DocumentsProvider() { } override fun queryChildDocuments(parentDocumentId: String, projection: Array?, sortOrder: String?): Cursor = runBlocking { - val result = MatrixCursor(DOCUMENT_PROJECTION) + val cursor = MatrixCursor(DEFAULT_DOCUMENT_PROJECTION) when (parentDocumentId) { - ROOT_DOC -> result.apply { - songRepository.getDownloadedSongs(SongSortInfoPreference).flow.first().forEach { song -> - val format = songRepository.getSongFormat(song.id).getValueAsync() - if (format != null) { - newRow() - .add(Document.COLUMN_DOCUMENT_ID, song.id) - .add(Document.COLUMN_DISPLAY_NAME, "${song.song.title}${mimeToExt(format.mimeType)}") - .add(Document.COLUMN_MIME_TYPE, format.mimeType) - .add(Document.COLUMN_SIZE, format.contentLength) - .add(Document.COLUMN_LAST_MODIFIED, song.song.modifyDate.atZone(ZoneOffset.UTC).toInstant().toEpochMilli()) + ROOT_DOC -> songRepository.getDownloadedSongs(SongSortInfoPreference).flow.first().forEach { song -> + val format = songRepository.getSongFormat(song.id).getValueAsync() + if (format != null) { + cursor.newRow() + .add(Document.COLUMN_DOCUMENT_ID, song.id) + .add(Document.COLUMN_DISPLAY_NAME, "${song.song.title}${mimeToExt(format.mimeType)}") + .add(Document.COLUMN_MIME_TYPE, format.mimeType) + .add(Document.COLUMN_SIZE, format.contentLength) + .add(Document.COLUMN_LAST_MODIFIED, song.song.modifyDate.atZone(ZoneOffset.UTC).toInstant().toEpochMilli()) // .add(Document.COLUMN_FLAGS, FLAG_SUPPORTS_THUMBNAIL) - } } } else -> {} } - result + cursor } override fun openDocument(documentId: String, mode: String?, signal: CancellationSignal?): ParcelFileDescriptor = runBlocking { @@ -108,10 +106,10 @@ class SongsProvider : DocumentsProvider() { } companion object { - private const val ROOT = "root" - private const val ROOT_DOC = "root_dir" + const val ROOT = "root" + const val ROOT_DOC = "root_dir" - private val ROOT_PROJECTION: Array = arrayOf( + private val DEFAULT_ROOT_PROJECTION: Array = arrayOf( Root.COLUMN_ROOT_ID, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_TITLE, @@ -121,7 +119,7 @@ class SongsProvider : DocumentsProvider() { Root.COLUMN_FLAGS, Root.COLUMN_AVAILABLE_BYTES ) - private val DOCUMENT_PROJECTION: Array = arrayOf( + private val DEFAULT_DOCUMENT_PROJECTION: Array = arrayOf( Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_MIME_TYPE, diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/SettingsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/SettingsFragment.kt index c0fc76924..3b1b32907 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/SettingsFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/SettingsFragment.kt @@ -22,8 +22,8 @@ class SettingsFragment : BaseSettingsFragment() { findNavController().navigate(R.id.playerAudioSettingsFragment) true } - findPreference(getString(R.string.pref_cache))?.setOnPreferenceClickListener { - findNavController().navigate(R.id.cacheSettingsFragment) + findPreference(getString(R.string.pref_storage))?.setOnPreferenceClickListener { + findNavController().navigate(R.id.storageSettingsFragment) true } findPreference(getString(R.string.pref_general))?.setOnPreferenceClickListener { diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/settings/CacheSettingsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/settings/StorageSettingsFragment.kt similarity index 80% rename from app/src/main/java/com/zionhuang/music/ui/fragments/settings/CacheSettingsFragment.kt rename to app/src/main/java/com/zionhuang/music/ui/fragments/settings/StorageSettingsFragment.kt index c54d79dac..3463032f6 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/settings/CacheSettingsFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/settings/StorageSettingsFragment.kt @@ -3,10 +3,12 @@ package com.zionhuang.music.ui.fragments.settings import android.content.ComponentName import android.content.Context.BIND_AUTO_CREATE import android.content.Intent +import android.content.Intent.ACTION_VIEW import android.content.ServiceConnection import android.os.Bundle import android.os.IBinder import android.text.format.Formatter +import androidx.core.net.toUri import androidx.preference.NeoSeekBarPreference import androidx.preference.Preference import coil.annotation.ExperimentalCoilApi @@ -17,7 +19,7 @@ import com.zionhuang.music.playback.MediaSessionConnection import com.zionhuang.music.playback.MusicService import com.zionhuang.music.ui.fragments.base.BaseSettingsFragment -class CacheSettingsFragment : BaseSettingsFragment() { +class StorageSettingsFragment : BaseSettingsFragment() { private val serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, iBinder: IBinder) { if (iBinder !is MusicService.MusicBinder) return @@ -35,8 +37,19 @@ class CacheSettingsFragment : BaseSettingsFragment() { @OptIn(ExperimentalCoilApi::class) override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - addPreferencesFromResource(R.xml.pref_cache) + addPreferencesFromResource(R.xml.pref_storage) + findPreference(getString(R.string.pref_open_saf))?.setOnPreferenceClickListener { + try { + startActivity(Intent(ACTION_VIEW, "content://${requireContext().packageName}.provider/root/root".toUri()).apply { + setPackage("com.google.android.documentsui") + setClassName("com.google.android.documentsui", "com.android.documentsui.files.FilesActivity") + }) + } catch (e: Exception) { + e.printStackTrace() + } + true + } val maxImageCacheSizePreference = findPreference(getString(R.string.pref_image_max_cache_size))!!.apply { setLabelFormatter { VALUE_TO_SIZE_TEXT[it] diff --git a/app/src/main/res/drawable/ic_folder.xml b/app/src/main/res/drawable/ic_folder.xml new file mode 100644 index 000000000..d90081c79 --- /dev/null +++ b/app/src/main/res/drawable/ic_folder.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/navigation/settings_navigation_graph.xml b/app/src/main/res/navigation/settings_navigation_graph.xml index 9f57e60bf..094ba0973 100644 --- a/app/src/main/res/navigation/settings_navigation_graph.xml +++ b/app/src/main/res/navigation/settings_navigation_graph.xml @@ -20,9 +20,9 @@ android:name="com.zionhuang.music.ui.fragments.settings.PlayerAudioSettingsFragment" android:label="@string/pref_player_audio_title" /> + android:id="@+id/storageSettingsFragment" + android:name="com.zionhuang.music.ui.fragments.settings.StorageSettingsFragment" + android:label="@string/pref_storage_title" /> Audio normalization Equalizer + Storage + View downloaded files in SAF Cache Max image cache size Clear image cache diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index b1130ab6c..4526951c6 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -43,6 +43,8 @@ Audio normalization Equalizer + Storage + View downloaded files in SAF Cache Max image cache size Clear image cache diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index 22b161f85..d3176f55d 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -43,6 +43,8 @@ Audio normalization اکولایزر + Storage + View downloaded files in SAF Cache Max image cache size Clear image cache diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 072c39af8..be18cd19f 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -43,6 +43,8 @@ Audio normalization Equalizer + Storage + View downloaded files in SAF Cache Max image cache size Clear image cache diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index a284e23c5..786226445 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -43,6 +43,8 @@ Audio normalization Equalizer + Storage + View downloaded files in SAF Cache Max image cache size Clear image cache diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 381379974..f905d8861 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -43,6 +43,8 @@ Audio normalization Equalizer + Storage + View downloaded files in SAF Gyorsítótár Max. kép gyorsítótár Kép gyors.tár törlés diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 02bd75a1b..e55929456 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -43,6 +43,8 @@ Audio normalization Equalizzatore + Storage + View downloaded files in SAF Cache Grandezza massima della cache delle immagini Pulisci cache delle immagini diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 824258e72..7d3d33a02 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -43,6 +43,8 @@ Audio normalization イコライザー + Storage + View downloaded files in SAF Cache Max image cache size Clear image cache diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 1a1b6419f..c09cf7d0b 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -43,6 +43,8 @@ Audio normalization Equalizer + Storage + View downloaded files in SAF Cache Max image cache size Clear image cache diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml index bebabba56..a67d3340a 100644 --- a/app/src/main/res/values-ml-rIN/strings.xml +++ b/app/src/main/res/values-ml-rIN/strings.xml @@ -43,6 +43,8 @@ Audio normalization ഇക്വലൈസർ + Storage + View downloaded files in SAF Cache Max image cache size Clear image cache diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 7a54201fe..80c4dd6ef 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -43,6 +43,8 @@ Audio normalization Equalizador + Storage + View downloaded files in SAF Cache Tamanho máximo do chace de imagens Limpar cache de imagens diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index e476f1cee..52095c69d 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -43,6 +43,8 @@ Audio normalization Equalizer + Storage + View downloaded files in SAF Cache Max image cache size Clear image cache diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index f7d6743d6..45409a237 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -43,6 +43,8 @@ 标准化音量 均衡器 + Storage + View downloaded files in SAF 缓存 最大图像缓存大小 清除图像缓存 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 2f7d9c40d..976b577a2 100755 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -39,10 +39,12 @@ 保留播放佇列 - 跳過歌曲頭尾無聲片段 + 跳過無聲片段 標準化音量 等化器 + 儲存 + 檢視已下載的歌曲 快取 圖片快取大小 清除圖片快取 diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index 6484b47e2..b4272f7bf 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -12,7 +12,7 @@ APPEARANCE CONTENT PLAYER_AUDIO - CACHE + STORAGE GENERAL PRIVACY BACKUP_RESTORE @@ -40,6 +40,7 @@ EXPAND_ON_PLAY EQUALIZER + OPEN_SAF IMAGE_MAX_CACHE_SIZE CLEAR_IMAGE_CACHE SONG_MAX_CACHE_SIZE diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7d545485f..8fa4aab98 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -43,6 +43,8 @@ Audio normalization Equalizer + Storage + View downloaded files in SAF Cache Max image cache size Clear image cache diff --git a/app/src/main/res/xml/pref_cache.xml b/app/src/main/res/xml/pref_cache.xml deleted file mode 100644 index 6331df2c7..000000000 --- a/app/src/main/res/xml/pref_cache.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/pref_main.xml b/app/src/main/res/xml/pref_main.xml index 52b96c8c5..2fba7ef0c 100644 --- a/app/src/main/res/xml/pref_main.xml +++ b/app/src/main/res/xml/pref_main.xml @@ -14,8 +14,8 @@ android:title="@string/pref_player_audio_title" /> + android:key="@string/pref_storage" + android:title="@string/pref_storage_title" /> + + + + + + + + + + \ No newline at end of file From 3b174b1937f796d952b2db0453193153a4a4976c Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Fri, 28 Oct 2022 19:46:32 +0800 Subject: [PATCH 50/95] Fix #365 --- app/src/main/res/values-fr-rFR/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 786226445..25e80706c 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -260,6 +260,7 @@ Start downloading %d song Start downloading %d songs + Start downloading %d songs Removed download View From 3eace47cd7d6826721740788d4730105fdddfc52 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Fri, 28 Oct 2022 22:28:13 +0800 Subject: [PATCH 51/95] Support choose directory in SAF --- .../zionhuang/music/provider/SongsProvider.kt | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/provider/SongsProvider.kt b/app/src/main/java/com/zionhuang/music/provider/SongsProvider.kt index f60877aa3..bb74cd561 100644 --- a/app/src/main/java/com/zionhuang/music/provider/SongsProvider.kt +++ b/app/src/main/java/com/zionhuang/music/provider/SongsProvider.kt @@ -33,7 +33,8 @@ class SongsProvider : DocumentsProvider() { .add(Root.COLUMN_TITLE, context!!.getString(R.string.app_name)) .add(Root.COLUMN_ICON, R.drawable.ic_launcher_foreground) .add(Root.COLUMN_MIME_TYPES, "*/*") - .add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_SEARCH) + .add(Root.COLUMN_AVAILABLE_BYTES, context!!.filesDir.freeSpace) + .add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_SEARCH or Root.FLAG_SUPPORTS_IS_CHILD) } override fun queryDocument(documentId: String, projection: Array?): Cursor = runBlocking { @@ -41,7 +42,7 @@ class SongsProvider : DocumentsProvider() { when (documentId) { ROOT_DOC -> cursor.newRow() .add(Document.COLUMN_DOCUMENT_ID, documentId) - .add(Document.COLUMN_DISPLAY_NAME, "Root") + .add(Document.COLUMN_DISPLAY_NAME, context!!.getString(R.string.app_name)) .add(Document.COLUMN_MIME_TYPE, MIME_TYPE_DIR) // .add(Document.COLUMN_FLAGS, FLAG_SUPPORTS_THUMBNAIL) else -> { @@ -79,11 +80,16 @@ class SongsProvider : DocumentsProvider() { cursor } - override fun openDocument(documentId: String, mode: String?, signal: CancellationSignal?): ParcelFileDescriptor = runBlocking { + override fun openDocument(documentId: String, mode: String, signal: CancellationSignal?): ParcelFileDescriptor = runBlocking { val file = songRepository.getSongFile(documentId) ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode)) } + override fun isChildDocument(parentDocumentId: String, documentId: String): Boolean = runBlocking { + val song = songRepository.getSongById(documentId).getValueAsync() + song != null && parentDocumentId == ROOT_DOC + } + private fun mimeToExt(mimeType: String) = when (FileTypes.inferFileTypeFromMimeType(mimeType)) { FileTypes.AC3 -> ".ac3" FileTypes.AC4 -> ".ac4" @@ -109,17 +115,17 @@ class SongsProvider : DocumentsProvider() { const val ROOT = "root" const val ROOT_DOC = "root_dir" - private val DEFAULT_ROOT_PROJECTION: Array = arrayOf( + private val DEFAULT_ROOT_PROJECTION = arrayOf( Root.COLUMN_ROOT_ID, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_TITLE, Root.COLUMN_SUMMARY, Root.COLUMN_ICON, Root.COLUMN_MIME_TYPES, - Root.COLUMN_FLAGS, - Root.COLUMN_AVAILABLE_BYTES + Root.COLUMN_AVAILABLE_BYTES, + Root.COLUMN_FLAGS ) - private val DEFAULT_DOCUMENT_PROJECTION: Array = arrayOf( + private val DEFAULT_DOCUMENT_PROJECTION = arrayOf( Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_MIME_TYPE, From 3c2791c2d10ce2cecff0acced8c30400e59f7c91 Mon Sep 17 00:00:00 2001 From: Sdarfeesh <50188628+Sdarfeesh@users.noreply.github.com> Date: Sat, 29 Oct 2022 10:43:46 +0800 Subject: [PATCH 52/95] Update Simplified Chinese Translation --- app/src/main/res/values-zh-rCN/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 45409a237..6422c886c 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -43,8 +43,8 @@ 标准化音量 均衡器 - Storage - View downloaded files in SAF + 存储 + 查看已下载的歌曲 缓存 最大图像缓存大小 清除图像缓存 From af8008b86537de2aec0d71dbc981d7710b7f733c Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sat, 29 Oct 2022 14:23:23 +0800 Subject: [PATCH 53/95] Support search in SAF (#117) --- .../com/zionhuang/music/db/daos/SongDao.kt | 5 ++ .../zionhuang/music/provider/SongsProvider.kt | 78 +++++++++++-------- .../zionhuang/music/repos/SongRepository.kt | 1 + .../music/repos/base/LocalRepository.kt | 1 + 4 files changed, 54 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/db/daos/SongDao.kt b/app/src/main/java/com/zionhuang/music/db/daos/SongDao.kt index 5ed9afa66..8824e99d7 100644 --- a/app/src/main/java/com/zionhuang/music/db/daos/SongDao.kt +++ b/app/src/main/java/com/zionhuang/music/db/daos/SongDao.kt @@ -79,6 +79,11 @@ interface SongDao { @Query("SELECT * FROM song WHERE title LIKE '%' || :query || '%' AND NOT isTrash") fun searchSongs(query: String): Flow> + @Transaction + @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) + @Query("SELECT * FROM song WHERE download_state = $STATE_DOWNLOADED AND title LIKE '%' || :query || '%'") + fun searchDownloadedSongs(query: String): Flow> + @Transaction @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) @Query("SELECT * FROM song WHERE title LIKE '%' || :query || '%' AND NOT isTrash LIMIT :previewSize") diff --git a/app/src/main/java/com/zionhuang/music/provider/SongsProvider.kt b/app/src/main/java/com/zionhuang/music/provider/SongsProvider.kt index bb74cd561..fa97243e2 100644 --- a/app/src/main/java/com/zionhuang/music/provider/SongsProvider.kt +++ b/app/src/main/java/com/zionhuang/music/provider/SongsProvider.kt @@ -38,46 +38,62 @@ class SongsProvider : DocumentsProvider() { } override fun queryDocument(documentId: String, projection: Array?): Cursor = runBlocking { - val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION) - when (documentId) { - ROOT_DOC -> cursor.newRow() - .add(Document.COLUMN_DOCUMENT_ID, documentId) - .add(Document.COLUMN_DISPLAY_NAME, context!!.getString(R.string.app_name)) - .add(Document.COLUMN_MIME_TYPE, MIME_TYPE_DIR) -// .add(Document.COLUMN_FLAGS, FLAG_SUPPORTS_THUMBNAIL) - else -> { - val song = songRepository.getSongById(documentId).getValueAsync() ?: throw FileNotFoundException() - val format = songRepository.getSongFormat(documentId).getValueAsync() ?: throw FileNotFoundException() - cursor.newRow() + MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION).apply { + when (documentId) { + ROOT_DOC -> newRow() .add(Document.COLUMN_DOCUMENT_ID, documentId) - .add(Document.COLUMN_DISPLAY_NAME, song.song.title) - .add(Document.COLUMN_MIME_TYPE, format.mimeType) - .add(Document.COLUMN_SIZE, format.contentLength) - .add(Document.COLUMN_LAST_MODIFIED, song.song.modifyDate.atZone(ZoneOffset.UTC).toInstant().toEpochMilli()) -// .add(Document.COLUMN_FLAGS, FLAG_SUPPORTS_THUMBNAIL) + .add(Document.COLUMN_DISPLAY_NAME, context!!.getString(R.string.app_name)) + .add(Document.COLUMN_MIME_TYPE, MIME_TYPE_DIR) + else -> { + val song = songRepository.getSongById(documentId).getValueAsync() ?: throw FileNotFoundException() + val format = songRepository.getSongFormat(documentId).getValueAsync() ?: throw FileNotFoundException() + newRow() + .add(Document.COLUMN_DOCUMENT_ID, documentId) + .add(Document.COLUMN_DISPLAY_NAME, song.song.title) + .add(Document.COLUMN_MIME_TYPE, format.mimeType) + .add(Document.COLUMN_SIZE, format.contentLength) + .add(Document.COLUMN_LAST_MODIFIED, song.song.modifyDate.atZone(ZoneOffset.UTC).toInstant().toEpochMilli()) + } } } - cursor } override fun queryChildDocuments(parentDocumentId: String, projection: Array?, sortOrder: String?): Cursor = runBlocking { - val cursor = MatrixCursor(DEFAULT_DOCUMENT_PROJECTION) - when (parentDocumentId) { - ROOT_DOC -> songRepository.getDownloadedSongs(SongSortInfoPreference).flow.first().forEach { song -> - val format = songRepository.getSongFormat(song.id).getValueAsync() - if (format != null) { - cursor.newRow() - .add(Document.COLUMN_DOCUMENT_ID, song.id) - .add(Document.COLUMN_DISPLAY_NAME, "${song.song.title}${mimeToExt(format.mimeType)}") - .add(Document.COLUMN_MIME_TYPE, format.mimeType) - .add(Document.COLUMN_SIZE, format.contentLength) - .add(Document.COLUMN_LAST_MODIFIED, song.song.modifyDate.atZone(ZoneOffset.UTC).toInstant().toEpochMilli()) -// .add(Document.COLUMN_FLAGS, FLAG_SUPPORTS_THUMBNAIL) + MatrixCursor(DEFAULT_DOCUMENT_PROJECTION).apply { + when (parentDocumentId) { + ROOT_DOC -> songRepository.getDownloadedSongs(SongSortInfoPreference).flow.first().forEach { song -> + val format = songRepository.getSongFormat(song.id).getValueAsync() + if (format != null) { + newRow() + .add(Document.COLUMN_DOCUMENT_ID, song.id) + .add(Document.COLUMN_DISPLAY_NAME, "${song.song.title}${mimeToExt(format.mimeType)}") + .add(Document.COLUMN_MIME_TYPE, format.mimeType) + .add(Document.COLUMN_SIZE, format.contentLength) + .add(Document.COLUMN_LAST_MODIFIED, song.song.modifyDate.atZone(ZoneOffset.UTC).toInstant().toEpochMilli()) + } + } + } + } + } + + override fun querySearchDocuments(rootId: String, query: String, projection: Array?): Cursor = runBlocking { + MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION).apply { + when (rootId) { + ROOT -> { + songRepository.searchDownloadedSongs(query).first().forEach { song -> + val format = songRepository.getSongFormat(song.id).getValueAsync() + if (format != null) { + newRow() + .add(Document.COLUMN_DOCUMENT_ID, song.id) + .add(Document.COLUMN_DISPLAY_NAME, "${song.song.title}${mimeToExt(format.mimeType)}") + .add(Document.COLUMN_MIME_TYPE, format.mimeType) + .add(Document.COLUMN_SIZE, format.contentLength) + .add(Document.COLUMN_LAST_MODIFIED, song.song.modifyDate.atZone(ZoneOffset.UTC).toInstant().toEpochMilli()) + } + } } } - else -> {} } - cursor } override fun openDocument(documentId: String, mode: String, signal: CancellationSignal?): ParcelFileDescriptor = runBlocking { diff --git a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt index 48a2427c9..129ccf2e3 100644 --- a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt +++ b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt @@ -202,6 +202,7 @@ class SongRepository(val context: Context) : LocalRepository { ) { songResult, artistResult, albumResult, playlistResult -> songResult + artistResult + albumResult + playlistResult } override fun searchSongs(query: String) = songDao.searchSongs(query) + override fun searchDownloadedSongs(query: String) = songDao.searchDownloadedSongs(query) override fun searchArtists(query: String) = artistDao.searchArtists(query) override fun searchAlbums(query: String) = albumDao.searchAlbums(query) override fun searchPlaylists(query: String) = playlistDao.searchPlaylists(query) diff --git a/app/src/main/java/com/zionhuang/music/repos/base/LocalRepository.kt b/app/src/main/java/com/zionhuang/music/repos/base/LocalRepository.kt index 1a6c25615..5c90da4c6 100644 --- a/app/src/main/java/com/zionhuang/music/repos/base/LocalRepository.kt +++ b/app/src/main/java/com/zionhuang/music/repos/base/LocalRepository.kt @@ -41,6 +41,7 @@ interface LocalRepository { */ fun searchAll(query: String): Flow> fun searchSongs(query: String): Flow> + fun searchDownloadedSongs(query: String): Flow> fun searchArtists(query: String): Flow> fun searchAlbums(query: String): Flow> fun searchPlaylists(query: String): Flow> From 9a04d96a04ed4502583f40879a3c3697b8df82a9 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sat, 29 Oct 2022 15:18:55 +0800 Subject: [PATCH 54/95] Fix #368 --- app/src/main/res/values-es-rUS/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fa-rIR/strings.xml | 1 + app/src/main/res/values-fi-rFI/strings.xml | 1 + app/src/main/res/values-fr-rFR/strings.xml | 1 + app/src/main/res/values-hu/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-ja-rJP/strings.xml | 1 + app/src/main/res/values-ko-rKR/strings.xml | 1 + app/src/main/res/values-ml-rIN/strings.xml | 1 + app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-sv-rSE/strings.xml | 1 + app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/pref_storage.xml | 1 + 16 files changed, 16 insertions(+) diff --git a/app/src/main/res/values-es-rUS/strings.xml b/app/src/main/res/values-es-rUS/strings.xml index a80d900e5..318c37ed6 100644 --- a/app/src/main/res/values-es-rUS/strings.xml +++ b/app/src/main/res/values-es-rUS/strings.xml @@ -45,6 +45,7 @@ Storage View downloaded files in SAF + This may not work in some devices Cache Max image cache size Clear image cache diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 4526951c6..e41f62993 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -45,6 +45,7 @@ Storage View downloaded files in SAF + This may not work in some devices Cache Max image cache size Clear image cache diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index d3176f55d..aa51f8c21 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -45,6 +45,7 @@ Storage View downloaded files in SAF + This may not work in some devices Cache Max image cache size Clear image cache diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index be18cd19f..5565c2b35 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -45,6 +45,7 @@ Storage View downloaded files in SAF + This may not work in some devices Cache Max image cache size Clear image cache diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 25e80706c..b24d5f5b0 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -45,6 +45,7 @@ Storage View downloaded files in SAF + This may not work in some devices Cache Max image cache size Clear image cache diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index f905d8861..507f5c696 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -45,6 +45,7 @@ Storage View downloaded files in SAF + This may not work in some devices Gyorsítótár Max. kép gyorsítótár Kép gyors.tár törlés diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index e55929456..4fd079e2d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -45,6 +45,7 @@ Storage View downloaded files in SAF + This may not work in some devices Cache Grandezza massima della cache delle immagini Pulisci cache delle immagini diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 7d3d33a02..d78e97566 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -45,6 +45,7 @@ Storage View downloaded files in SAF + This may not work in some devices Cache Max image cache size Clear image cache diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index c09cf7d0b..6dbcd9c17 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -45,6 +45,7 @@ Storage View downloaded files in SAF + This may not work in some devices Cache Max image cache size Clear image cache diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml index a67d3340a..4360fe23e 100644 --- a/app/src/main/res/values-ml-rIN/strings.xml +++ b/app/src/main/res/values-ml-rIN/strings.xml @@ -45,6 +45,7 @@ Storage View downloaded files in SAF + This may not work in some devices Cache Max image cache size Clear image cache diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 80c4dd6ef..6d2a02e62 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -45,6 +45,7 @@ Storage View downloaded files in SAF + This may not work in some devices Cache Tamanho máximo do chace de imagens Limpar cache de imagens diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 52095c69d..f6ad9ff78 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -45,6 +45,7 @@ Storage View downloaded files in SAF + This may not work in some devices Cache Max image cache size Clear image cache diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 6422c886c..a3f28d3a0 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -45,6 +45,7 @@ 存储 查看已下载的歌曲 + This may not work in some devices 缓存 最大图像缓存大小 清除图像缓存 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 976b577a2..1ccbe8ba5 100755 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -45,6 +45,7 @@ 儲存 檢視已下載的歌曲 + 在某些裝置可能無法開啟 快取 圖片快取大小 清除圖片快取 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8fa4aab98..c19a81470 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -45,6 +45,7 @@ Storage View downloaded files in SAF + This may not work in some devices Cache Max image cache size Clear image cache diff --git a/app/src/main/res/xml/pref_storage.xml b/app/src/main/res/xml/pref_storage.xml index 1d7b3a5c1..5fafebabb 100644 --- a/app/src/main/res/xml/pref_storage.xml +++ b/app/src/main/res/xml/pref_storage.xml @@ -5,6 +5,7 @@ From 943a0097e80d2c5320591a5227751b1141f57993 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sat, 29 Oct 2022 15:42:59 +0800 Subject: [PATCH 55/95] Animate layout change for player thumbnail view --- app/src/main/res/layout/bottom_controls_sheet.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/bottom_controls_sheet.xml b/app/src/main/res/layout/bottom_controls_sheet.xml index 9be196341..f195b6f23 100644 --- a/app/src/main/res/layout/bottom_controls_sheet.xml +++ b/app/src/main/res/layout/bottom_controls_sheet.xml @@ -38,6 +38,7 @@ Date: Sat, 29 Oct 2022 16:07:37 +0800 Subject: [PATCH 56/95] Remove selection change animation --- .../music/ui/viewholders/LocalItemViewHolder.kt | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/ui/viewholders/LocalItemViewHolder.kt b/app/src/main/java/com/zionhuang/music/ui/viewholders/LocalItemViewHolder.kt index c48ec8845..c131b41bb 100644 --- a/app/src/main/java/com/zionhuang/music/ui/viewholders/LocalItemViewHolder.kt +++ b/app/src/main/java/com/zionhuang/music/ui/viewholders/LocalItemViewHolder.kt @@ -80,8 +80,7 @@ open class SongViewHolder( } override fun onSelectionChanged(isSelected: Boolean) { - if (isSelected) binding.selectedIndicator.fadeIn(binding.context.resources.getInteger(R.integer.motion_duration_small).toLong()) - else binding.selectedIndicator.fadeOut(binding.context.resources.getInteger(R.integer.motion_duration_small).toLong()) + binding.selectedIndicator.isVisible = isSelected } } @@ -126,8 +125,7 @@ class ArtistViewHolder( } override fun onSelectionChanged(isSelected: Boolean) { - if (isSelected) binding.selectedIndicator.fadeIn(binding.context.resources.getInteger(R.integer.motion_duration_small).toLong()) - else binding.selectedIndicator.fadeOut(binding.context.resources.getInteger(R.integer.motion_duration_small).toLong()) + binding.selectedIndicator.isVisible = isSelected } } @@ -170,8 +168,7 @@ class AlbumViewHolder( } override fun onSelectionChanged(isSelected: Boolean) { - if (isSelected) binding.selectedIndicator.fadeIn(binding.context.resources.getInteger(R.integer.motion_duration_small).toLong()) - else binding.selectedIndicator.fadeOut(binding.context.resources.getInteger(R.integer.motion_duration_small).toLong()) + binding.selectedIndicator.isVisible = isSelected } } @@ -222,8 +219,7 @@ class PlaylistViewHolder( } override fun onSelectionChanged(isSelected: Boolean) { - if (isSelected) binding.selectedIndicator.fadeIn(binding.context.resources.getInteger(R.integer.motion_duration_small).toLong()) - else binding.selectedIndicator.fadeOut(binding.context.resources.getInteger(R.integer.motion_duration_small).toLong()) + binding.selectedIndicator.isVisible = isSelected } } @@ -287,8 +283,7 @@ class CustomPlaylistViewHolder( } override fun onSelectionChanged(isSelected: Boolean) { - if (isSelected) binding.selectedIndicator.fadeIn(binding.context.resources.getInteger(R.integer.motion_duration_small).toLong()) - else binding.selectedIndicator.fadeOut(binding.context.resources.getInteger(R.integer.motion_duration_small).toLong()) + binding.selectedIndicator.isVisible = isSelected } } From 23c8523bbf5e2cdd952f570a027ec845a136183e Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sat, 29 Oct 2022 16:08:25 +0800 Subject: [PATCH 57/95] Remove selection change animation --- .../com/zionhuang/music/ui/viewholders/YouTubeViewHolder.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/ui/viewholders/YouTubeViewHolder.kt b/app/src/main/java/com/zionhuang/music/ui/viewholders/YouTubeViewHolder.kt index 48d666245..d2c553c90 100644 --- a/app/src/main/java/com/zionhuang/music/ui/viewholders/YouTubeViewHolder.kt +++ b/app/src/main/java/com/zionhuang/music/ui/viewholders/YouTubeViewHolder.kt @@ -18,8 +18,6 @@ import com.zionhuang.innertube.models.SuggestionTextItem.SuggestionSource.LOCAL import com.zionhuang.music.R import com.zionhuang.music.databinding.* import com.zionhuang.music.extensions.context -import com.zionhuang.music.extensions.fadeIn -import com.zionhuang.music.extensions.fadeOut import com.zionhuang.music.extensions.show import com.zionhuang.music.repos.SongRepository import com.zionhuang.music.ui.adapters.YouTubeItemAdapter @@ -165,8 +163,7 @@ class YouTubeListItemViewHolder( } fun onSelectionChanged(isSelected: Boolean) { - if (isSelected) binding.selectedIndicator.fadeIn(binding.context.resources.getInteger(R.integer.motion_duration_small).toLong()) - else binding.selectedIndicator.fadeOut(binding.context.resources.getInteger(R.integer.motion_duration_small).toLong()) + binding.selectedIndicator.isVisible = isSelected } } From 795c987ad3f48ae9ccaeb293663e99b68337cdc7 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sat, 29 Oct 2022 17:40:52 +0800 Subject: [PATCH 58/95] Update readme --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b40ff7395..b145fb748 100644 --- a/README.md +++ b/README.md @@ -36,17 +36,21 @@ With this app, you're like getting a free music streaming service. You can liste - Save songs, albums and playlists in local database - Download music for offline playback -- Edit song title +- Like songs +- local playlist management - Add links to your favorite YouTube Music playlists +- Export downloaded songs via SAF ### Player - Material design player - Lockscreen playback -- Media controls in notification -- Skip to next/previous song -- Repeat/shuffle mode -- Edit now-playing queue +- Cache songs +- (Synchronized) lyrics +- Skip silence +- Audio normalization +- Stat for nerds +- Persistent queue ### Other @@ -55,6 +59,7 @@ With this app, you're like getting a free music streaming service. You can liste - Localization - Proxy - Backup & restore +- Support Android Auto ## Screenshots From 3573bdf8671d67064feada8d38423b799cc1683a Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sat, 29 Oct 2022 17:40:58 +0800 Subject: [PATCH 59/95] Update gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 675cd0349..7a638a3dd 100755 --- a/.gitignore +++ b/.gitignore @@ -85,4 +85,5 @@ lint/outputs/ lint/tmp/ # lint/reports/ -.DS_Store \ No newline at end of file +.DS_Store +/app/release/output-metadata.json From 298e1e0d776950c54464bd9490f65ab1d4799180 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sat, 29 Oct 2022 17:53:08 +0800 Subject: [PATCH 60/95] Fix typo --- app/src/main/res/layout/bottom_controls_sheet.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/bottom_controls_sheet.xml b/app/src/main/res/layout/bottom_controls_sheet.xml index f195b6f23..0c99a43c5 100644 --- a/app/src/main/res/layout/bottom_controls_sheet.xml +++ b/app/src/main/res/layout/bottom_controls_sheet.xml @@ -110,7 +110,7 @@ app:lrcNormalTextSize="20sp" app:lrcPadding="16dp" app:lrcTextGravity="center" - app:lrcTextSize="2sp" + app:lrcTextSize="24sp" app:lrcTimelineColor="#00000000" app:lrcTimelineTextColor="@color/m3_dark_default_color_secondary_text" app:lrcUnsyncedTextColor="@color/m3_dark_default_color_primary_text" From aff1ce32c8729b5bebc7bc36b92f0b1c25825ad6 Mon Sep 17 00:00:00 2001 From: gidano Date: Sat, 29 Oct 2022 15:00:55 +0200 Subject: [PATCH 61/95] Edited hungarian strings Text editing and correction --- app/src/main/res/values-hu/strings.xml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 507f5c696..da12a6a5b 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -39,13 +39,13 @@ Magas Gyenge Állandó várósor - Skip silence - Audio normalization + A csend átugrása + Hang normalizálása Equalizer - Storage - View downloaded files in SAF - This may not work in some devices + Tárhely + A letöltött fájlok megtekintése SAF-ben + Előfordulhat, hogy ez egyes eszközökön nem működik Gyorsítótár Max. kép gyorsítótár Kép gyors.tár törlés @@ -63,7 +63,7 @@ Keresési előzmények szüneteltetése Keresési előzmények törlése Biztosan töröl minden keresési előzményt? - Enable KuGou lyrics provider + A KuGou dalszövegszolgáltató engedélyezése Bizt.mentés és visszaállítás Bizt.mentés @@ -127,8 +127,8 @@ Kodekek Bitráta Mintavétel - Hangerősség - Volume + Hangosság + Hangerő Fájl méret Ismeretlen @@ -150,7 +150,7 @@ Duplikált előadók %1$s előadó már létezik. - Válassz lejátszási listát + Válasszon lejátszási listát Lejátszólista szerkesztés @@ -216,19 +216,19 @@ Visszavon Az URL nem azonosítható. - Dal szól legközelebb + dal szól legközelebb %d dal szól legközelebb - Előadó szól legközelebb + előadó szól legközelebb %d előadó szól legközelebb - Album szól legközelebb + album szól legközelebb %d album szól legközelebb - Lista szól legközelebb + lista szól legközelebb %d lista szól legközelebb A kiválasztottak szólnak legközelebb From a49c59110cf1ca642cf37385fac0e4c430cb5b4d Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sat, 29 Oct 2022 23:58:23 +0800 Subject: [PATCH 62/95] Revert animate layout change --- .../res/layout-land/bottom_controls_sheet.xml | 504 +++++++++--------- .../main/res/layout/bottom_controls_sheet.xml | 1 - 2 files changed, 249 insertions(+), 256 deletions(-) diff --git a/app/src/main/res/layout-land/bottom_controls_sheet.xml b/app/src/main/res/layout-land/bottom_controls_sheet.xml index 7ba2badfd..b8d05b8d2 100644 --- a/app/src/main/res/layout-land/bottom_controls_sheet.xml +++ b/app/src/main/res/layout-land/bottom_controls_sheet.xml @@ -20,285 +20,279 @@ type="com.zionhuang.music.viewmodels.PlaybackViewModel" /> - + android:focusable="true" + android:orientation="horizontal" + android:padding="16dp"> + android:layout_weight="4"> - + android:clickable="true" + android:focusable="true"> - + - + + + + + - - - - - - - - - - - - - + + - + android:gravity="center" + android:padding="6dp" + android:text="@{viewModel.playbackState.errorMessage}" + android:textColor="?android:textColorPrimaryInverse" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/error_icon" + app:layout_constraintTop_toTopOf="parent" + tools:text="Error message" /> + + + + + + - - + + - + + + + + + + - + - - + android:layout_weight="1" + tools:text="0:00" /> - - - - - - - - - - - - - - - - - - - - - + tools:text="4:00" /> + - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_controls_sheet.xml b/app/src/main/res/layout/bottom_controls_sheet.xml index 0c99a43c5..9be196341 100644 --- a/app/src/main/res/layout/bottom_controls_sheet.xml +++ b/app/src/main/res/layout/bottom_controls_sheet.xml @@ -38,7 +38,6 @@ Date: Sat, 29 Oct 2022 23:58:46 +0800 Subject: [PATCH 63/95] Enhance LyricsView --- .../zionhuang/music/ui/fragments/BottomControlsFragment.kt | 6 +++--- .../main/java/com/zionhuang/music/ui/widgets/LyricsView.kt | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt index 2b43a0e40..7761c4df9 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt @@ -16,7 +16,7 @@ import com.zionhuang.innertube.models.BrowseLocalArtistSongsEndpoint import com.zionhuang.music.R import com.zionhuang.music.constants.MediaSessionConstants.ACTION_TOGGLE_LIKE import com.zionhuang.music.databinding.BottomControlsSheetBinding -import com.zionhuang.music.db.entities.LyricsEntity +import com.zionhuang.music.db.entities.LyricsEntity.Companion.LYRICS_NOT_FOUND import com.zionhuang.music.playback.MediaSessionConnection import com.zionhuang.music.ui.activities.MainActivity import com.zionhuang.music.utils.NavigationEndpointHandler @@ -130,10 +130,10 @@ class BottomControlsFragment : Fragment() { } lifecycleScope.launch { viewModel.currentLyrics.collectLatest { lyrics -> - if (lyrics == null || lyrics.lyrics == LyricsEntity.LYRICS_NOT_FOUND) { + if (lyrics == null) { binding.lyricsView.reset() } else { - binding.lyricsView.loadLyrics(lyrics.lyrics) + binding.lyricsView.loadLyrics(lyrics.lyrics.takeIf { it != LYRICS_NOT_FOUND } ?: getString(R.string.lyrics_not_found)) } } } diff --git a/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt b/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt index 7ffd00a35..5f43999ff 100644 --- a/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt +++ b/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt @@ -54,6 +54,7 @@ class LyricsView @JvmOverloads constructor( private var drawableWidth = 0 private var timeTextWidth = 0 private var defaultLabel: String = "" + private var showLabel: Boolean = false private var lrcPadding = 0f private var onLyricsClickListener: OnLyricsClickListener? = null private var animator: ValueAnimator? = null @@ -290,7 +291,7 @@ class LyricsView @JvmOverloads constructor( super.onDraw(canvas) val centerY = height / 2 - if (!hasLyrics()) { + if (!hasLyrics() && showLabel) { lrcPaint.color = currentTextColor val staticLayout = StaticLayout.Builder.obtain(defaultLabel, 0, defaultLabel.length, lrcPaint, lrcWidth.toInt()) .setAlignment(Layout.Alignment.ALIGN_CENTER) From a4a67f73a03625879afb97c5db881dead9a73224 Mon Sep 17 00:00:00 2001 From: Sdarfeesh <50188628+Sdarfeesh@users.noreply.github.com> Date: Sun, 30 Oct 2022 10:14:46 +0800 Subject: [PATCH 64/95] Update Simplified Chinese Translation --- app/src/main/res/values-zh-rCN/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index a3f28d3a0..cda3fc64c 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -45,7 +45,7 @@ 存储 查看已下载的歌曲 - This may not work in some devices + 在某些设备可能无法打开 缓存 最大图像缓存大小 清除图像缓存 From 1425dc67b5d56be9914e41f95f348fb8b1545e41 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sun, 30 Oct 2022 13:17:30 +0800 Subject: [PATCH 65/95] Improve player layout (#76) --- app/build.gradle.kts | 1 + .../music/ui/activities/MainActivity.kt | 4 + .../ui/fragments/BottomControlsFragment.kt | 15 ++ .../zionhuang/music/ui/widgets/LyricsView.kt | 7 +- .../res/layout-land/bottom_controls_sheet.xml | 108 ++++++---- app/src/main/res/layout/activity_main.xml | 1 + .../main/res/layout/bottom_controls_sheet.xml | 194 +++++++++++------- 7 files changed, 215 insertions(+), 115 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 744700201..cca23c800 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -143,6 +143,7 @@ dependencies { implementation("androidx.transition:transition-ktx:1.4.1") implementation("com.google.android.material:material:1.8.0-alpha01") implementation("dev.chrisbanes.insetter:insetter:0.6.1") + implementation("com.github.bosphere.android-fadingedgelayout:fadingedgelayout:1.0.0") // Gson implementation("com.google.code.gson:gson:2.9.0") // ExoPlayer diff --git a/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt b/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt index 5519a7c0d..8555e7b76 100644 --- a/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt +++ b/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt @@ -5,6 +5,7 @@ import android.annotation.SuppressLint import android.content.Intent import android.content.Intent.EXTRA_TEXT import android.content.res.Configuration +import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.content.res.Configuration.ORIENTATION_PORTRAIT import android.os.Bundle import android.view.ActionMode @@ -163,6 +164,9 @@ class MainActivity : ThemedBindingActivity(), NavController } lifecycleScope.launch { AdaptiveUtils.orientation.collectLatest { orientation -> + binding.queueSheet.updateLayoutParams { + width = if (orientation == ORIENTATION_LANDSCAPE) (resources.displayMetrics.widthPixels * 0.6).toInt() else resources.displayMetrics.widthPixels + } binding.container.updateLayoutParams { bottomMargin = if (orientation == ORIENTATION_PORTRAIT) resources.getDimensionPixelSize(R.dimen.m3_bottom_nav_min_height) else 0 } diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt index 7761c4df9..2d28b82b0 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt @@ -17,6 +17,7 @@ import com.zionhuang.music.R import com.zionhuang.music.constants.MediaSessionConstants.ACTION_TOGGLE_LIKE import com.zionhuang.music.databinding.BottomControlsSheetBinding import com.zionhuang.music.db.entities.LyricsEntity.Companion.LYRICS_NOT_FOUND +import com.zionhuang.music.extensions.systemBarInsetsCompat import com.zionhuang.music.playback.MediaSessionConnection import com.zionhuang.music.ui.activities.MainActivity import com.zionhuang.music.utils.NavigationEndpointHandler @@ -45,6 +46,20 @@ class BottomControlsFragment : Fragment() { binding.viewModel = viewModel binding.root.applyInsetter { + type(navigationBars = true) { + padding() + } + } + binding.cardViewContainer.applyInsetter { + type(statusBars = true) { + padding() + } + } + binding.lyricsView.setOnApplyWindowInsetsListener { _, insets -> + binding.lyricsView.immersivePaddingTop = insets.systemBarInsetsCompat.top + insets + } + binding.rightPart?.applyInsetter { type(statusBars = true, navigationBars = true) { padding() } diff --git a/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt b/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt index 5f43999ff..4c167df61 100644 --- a/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt +++ b/app/src/main/java/com/zionhuang/music/ui/widgets/LyricsView.kt @@ -68,6 +68,7 @@ class LyricsView @JvmOverloads constructor( private var isFling = false private var textGravity = 0 // left/center/right private var isSyncedLyrics = true + var immersivePaddingTop = 0 var isPlaying = false set(value) { if (field == value) return @@ -455,8 +456,8 @@ class LyricsView @JvmOverloads constructor( var line = 0 var minDistance = Float.MAX_VALUE for (i in lrcEntryList.indices) { - if (abs(offset - getOffset(i)) < minDistance) { - minDistance = abs(offset - getOffset(i)) + if (abs(offset + immersivePaddingTop - getOffset(i)) < minDistance) { + minDistance = abs(offset + immersivePaddingTop - getOffset(i)) line = i } } @@ -466,7 +467,7 @@ class LyricsView @JvmOverloads constructor( private fun getOffset(line: Int): Float { if (lrcEntryList.isEmpty()) return 0F if (lrcEntryList[line].offset == Float.MIN_VALUE) { - var offset = (height / 2).toFloat() + var offset = ((height + immersivePaddingTop) / 2).toFloat() for (i in 1..line) { offset -= (lrcEntryList[i - 1].height + lrcEntryList[i].height) / 2 + dividerHeight } diff --git a/app/src/main/res/layout-land/bottom_controls_sheet.xml b/app/src/main/res/layout-land/bottom_controls_sheet.xml index b8d05b8d2..d2f41933f 100644 --- a/app/src/main/res/layout-land/bottom_controls_sheet.xml +++ b/app/src/main/res/layout-land/bottom_controls_sheet.xml @@ -23,27 +23,31 @@ + android:orientation="horizontal"> - + android:layout_weight="4" + android:paddingStart="16dp" + android:paddingEnd="8dp"> + android:focusable="true" + android:paddingVertical="12dp"> - - - - + - + + + + + + + + + android:layout_marginBottom="64dp" + android:layout_weight="6" + android:paddingStart="8dp" + android:paddingEnd="16dp"> diff --git a/app/src/main/res/layout/bottom_controls_sheet.xml b/app/src/main/res/layout/bottom_controls_sheet.xml index 9be196341..8327a0726 100644 --- a/app/src/main/res/layout/bottom_controls_sheet.xml +++ b/app/src/main/res/layout/bottom_controls_sheet.xml @@ -23,110 +23,149 @@ + android:paddingHorizontal="16dp"> - + android:layout_weight="1"> - + - + - + - + - - - - + + + + + + + + + + + + + + - - + - + @@ -203,7 +243,7 @@ android:layout_gravity="center_horizontal" android:layout_marginHorizontal="16dp" android:layout_marginTop="16dp" - android:layout_marginBottom="80dp" + android:layout_marginBottom="24dp" android:orientation="horizontal"> Date: Sun, 30 Oct 2022 18:17:16 +0800 Subject: [PATCH 66/95] [Feature] More action for lyrics (#76) --- .../music/constants/MediaConstants.kt | 1 + .../music/lyrics/KuGouLyricsProvider.kt | 4 ++ .../zionhuang/music/lyrics/LyricsProvider.kt | 2 + .../music/lyrics/YouTubeLyricsProvider.kt | 3 + .../zionhuang/music/playback/SongPlayer.kt | 23 +------ .../zionhuang/music/repos/SongRepository.kt | 2 +- .../music/ui/adapters/LyricsAdapter.kt | 22 +++++++ .../ui/fragments/BottomControlsFragment.kt | 61 +++++++++++++++++ .../fragments/dialogs/ChooseLyricsDialog.kt | 65 ++++++++++++++++++ .../ui/viewholders/LyricsItemViewHolder.kt | 28 ++++++++ .../music/utils/lyrics/LyricsHelper.kt | 63 ++++++++++++++++++ app/src/main/res/drawable/ic_search.xml | 13 ++-- app/src/main/res/drawable/ic_sync.xml | 10 +++ app/src/main/res/drawable/ic_visibility.xml | 10 +++ .../main/res/layout/dialog_choose_lyrics.xml | 24 +++++++ .../main/res/layout/dialog_edit_lyrics.xml | 25 +++++++ app/src/main/res/layout/item_lyrics.xml | 66 +++++++++++++++++++ app/src/main/res/menu/lyrics.xml | 19 ++++++ app/src/main/res/menu/search_and_settings.xml | 2 +- app/src/main/res/menu/search_icon.xml | 2 +- app/src/main/res/values-es-rUS/strings.xml | 6 +- app/src/main/res/values-es/strings.xml | 6 +- app/src/main/res/values-fa-rIR/strings.xml | 6 +- app/src/main/res/values-fi-rFI/strings.xml | 6 +- app/src/main/res/values-fr-rFR/strings.xml | 6 +- app/src/main/res/values-hu/strings.xml | 6 +- app/src/main/res/values-it/strings.xml | 6 +- app/src/main/res/values-ja-rJP/strings.xml | 6 +- app/src/main/res/values-ko-rKR/strings.xml | 6 +- app/src/main/res/values-ml-rIN/strings.xml | 6 +- app/src/main/res/values-pt-rBR/strings.xml | 6 +- app/src/main/res/values-sv-rSE/strings.xml | 6 +- app/src/main/res/values-zh-rCN/strings.xml | 6 +- app/src/main/res/values-zh-rTW/strings.xml | 6 +- app/src/main/res/values/strings.xml | 6 +- .../main/java/com/zionhuang/kugou/KuGou.kt | 37 +++++++---- 36 files changed, 515 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/com/zionhuang/music/ui/adapters/LyricsAdapter.kt create mode 100644 app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/ChooseLyricsDialog.kt create mode 100644 app/src/main/java/com/zionhuang/music/ui/viewholders/LyricsItemViewHolder.kt create mode 100644 app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsHelper.kt create mode 100644 app/src/main/res/drawable/ic_sync.xml create mode 100644 app/src/main/res/drawable/ic_visibility.xml create mode 100644 app/src/main/res/layout/dialog_choose_lyrics.xml create mode 100644 app/src/main/res/layout/dialog_edit_lyrics.xml create mode 100644 app/src/main/res/layout/item_lyrics.xml create mode 100644 app/src/main/res/menu/lyrics.xml diff --git a/app/src/main/java/com/zionhuang/music/constants/MediaConstants.kt b/app/src/main/java/com/zionhuang/music/constants/MediaConstants.kt index 09e47b2e1..a1b56d9a4 100644 --- a/app/src/main/java/com/zionhuang/music/constants/MediaConstants.kt +++ b/app/src/main/java/com/zionhuang/music/constants/MediaConstants.kt @@ -1,6 +1,7 @@ package com.zionhuang.music.constants object MediaConstants { + const val EXTRA_MEDIA_METADATA = "media_metadata" const val EXTRA_MEDIA_METADATA_ITEMS = "media_metadata_items" const val EXTRA_SONG = "song" const val EXTRA_ARTIST = "artist" diff --git a/app/src/main/java/com/zionhuang/music/lyrics/KuGouLyricsProvider.kt b/app/src/main/java/com/zionhuang/music/lyrics/KuGouLyricsProvider.kt index f2f6ccddd..e18c8e0a9 100644 --- a/app/src/main/java/com/zionhuang/music/lyrics/KuGouLyricsProvider.kt +++ b/app/src/main/java/com/zionhuang/music/lyrics/KuGouLyricsProvider.kt @@ -6,9 +6,13 @@ import com.zionhuang.music.R import com.zionhuang.music.extensions.sharedPreferences object KuGouLyricsProvider : LyricsProvider { + override val name = "Kugou" override fun isEnabled(context: Context): Boolean = context.sharedPreferences.getBoolean(context.getString(R.string.pref_enable_kugou), true) override suspend fun getLyrics(id: String, title: String, artist: String, duration: Int): Result = KuGou.getLyrics(title, artist, duration) + + override suspend fun getAllLyrics(id: String, title: String, artist: String, duration: Int): Result> = + KuGou.getAllLyrics(title, artist, duration) } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/lyrics/LyricsProvider.kt b/app/src/main/java/com/zionhuang/music/lyrics/LyricsProvider.kt index 71737a725..4ed49c917 100644 --- a/app/src/main/java/com/zionhuang/music/lyrics/LyricsProvider.kt +++ b/app/src/main/java/com/zionhuang/music/lyrics/LyricsProvider.kt @@ -3,6 +3,8 @@ package com.zionhuang.music.lyrics import android.content.Context interface LyricsProvider { + val name: String fun isEnabled(context: Context): Boolean suspend fun getLyrics(id: String, title: String, artist: String, duration: Int): Result + suspend fun getAllLyrics(id: String, title: String, artist: String, duration: Int): Result> } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/lyrics/YouTubeLyricsProvider.kt b/app/src/main/java/com/zionhuang/music/lyrics/YouTubeLyricsProvider.kt index fb0315e6c..56fc049bf 100644 --- a/app/src/main/java/com/zionhuang/music/lyrics/YouTubeLyricsProvider.kt +++ b/app/src/main/java/com/zionhuang/music/lyrics/YouTubeLyricsProvider.kt @@ -5,6 +5,7 @@ import com.zionhuang.innertube.YouTube import com.zionhuang.innertube.models.WatchEndpoint object YouTubeLyricsProvider : LyricsProvider { + override val name = "YouTube Music" override fun isEnabled(context: Context) = true override suspend fun getLyrics(id: String, title: String, artist: String, duration: Int): Result = YouTube.next(WatchEndpoint(videoId = id)).mapCatching { nextResult -> @@ -13,4 +14,6 @@ object YouTubeLyricsProvider : LyricsProvider { browseResult.lyrics ?: throw IllegalStateException("Lyrics unavailable") } + override suspend fun getAllLyrics(id: String, title: String, artist: String, duration: Int): Result> = + getLyrics(id, title, artist, duration).map { listOf(it) } } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt index 7932b0f3c..d05301e63 100644 --- a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt +++ b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt @@ -67,13 +67,8 @@ import com.zionhuang.music.constants.MediaSessionConstants.COMMAND_PLAY_NEXT import com.zionhuang.music.constants.MediaSessionConstants.COMMAND_SEEK_TO_QUEUE_ITEM import com.zionhuang.music.constants.MediaSessionConstants.EXTRA_QUEUE_INDEX import com.zionhuang.music.db.entities.FormatEntity -import com.zionhuang.music.db.entities.LyricsEntity -import com.zionhuang.music.db.entities.LyricsEntity.Companion.LYRICS_NOT_FOUND import com.zionhuang.music.db.entities.Song import com.zionhuang.music.extensions.* -import com.zionhuang.music.lyrics.KuGouLyricsProvider -import com.zionhuang.music.lyrics.LyricsProvider -import com.zionhuang.music.lyrics.YouTubeLyricsProvider import com.zionhuang.music.models.MediaMetadata import com.zionhuang.music.models.sortInfo.SongSortInfoPreference import com.zionhuang.music.playback.MusicService.Companion.ALBUM @@ -89,6 +84,7 @@ import com.zionhuang.music.ui.activities.MainActivity import com.zionhuang.music.ui.bindings.resizeThumbnailUrl import com.zionhuang.music.ui.fragments.settings.StorageSettingsFragment.Companion.VALUE_TO_MB import com.zionhuang.music.utils.InfoCache +import com.zionhuang.music.utils.lyrics.LyricsHelper import com.zionhuang.music.utils.preference.enumPreference import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers.IO @@ -132,7 +128,6 @@ class SongPlayer( var currentSong: Song? = null private val showLyrics = context.sharedPreferences.booleanFlow(context.getString(R.string.pref_show_lyrics), false) - private val lyricProviders: List = listOf(KuGouLyricsProvider, YouTubeLyricsProvider) val mediaSession = MediaSessionCompat(context, context.getString(R.string.app_name)).apply { isActive = true @@ -362,20 +357,6 @@ class SongPlayer( setUseFastForwardAction(false) } - private suspend fun loadLyrics(mediaMetadata: MediaMetadata) { - lyricProviders.forEach { provider -> - if (provider.isEnabled(context)) { - provider.getLyrics(mediaMetadata.id, mediaMetadata.title, mediaMetadata.artists.joinToString { it.name }, mediaMetadata.duration).onSuccess { lyrics -> - songRepository.upsert(LyricsEntity(mediaMetadata.id, lyrics)) - return - }.onFailure { - it.printStackTrace() - } - } - } - songRepository.upsert(LyricsEntity(mediaMetadata.id, LYRICS_NOT_FOUND)) - } - init { scope.launch { currentSongFlow.collect { song -> @@ -392,7 +373,7 @@ class SongPlayer( Pair(mediaMetadata, showLyrics) }.collectLatest { (mediaMetadata, showLyrics) -> if (showLyrics && mediaMetadata != null && !songRepository.hasLyrics(mediaMetadata.id)) { - loadLyrics(mediaMetadata) + LyricsHelper.loadLyrics(context, mediaMetadata) } } } diff --git a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt index 129ccf2e3..a0ce15276 100644 --- a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt +++ b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt @@ -38,7 +38,7 @@ import kotlinx.coroutines.withContext import java.io.File import java.time.LocalDateTime -class SongRepository(val context: Context) : LocalRepository { +class SongRepository(private val context: Context) : LocalRepository { private val database = MusicDatabase.getInstance(context) private val songDao = database.songDao private val artistDao = database.artistDao diff --git a/app/src/main/java/com/zionhuang/music/ui/adapters/LyricsAdapter.kt b/app/src/main/java/com/zionhuang/music/ui/adapters/LyricsAdapter.kt new file mode 100644 index 000000000..c1576235d --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/ui/adapters/LyricsAdapter.kt @@ -0,0 +1,22 @@ +package com.zionhuang.music.ui.adapters + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.zionhuang.music.R +import com.zionhuang.music.extensions.inflateWithBinding +import com.zionhuang.music.ui.viewholders.LyricsItemViewHolder +import com.zionhuang.music.utils.lyrics.LyricsHelper + +class LyricsAdapter : RecyclerView.Adapter() { + var items = emptyList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LyricsItemViewHolder = + LyricsItemViewHolder(parent.inflateWithBinding(R.layout.item_lyrics)) + + + override fun onBindViewHolder(holder: LyricsItemViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount(): Int = items.size +} \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt index 2d28b82b0..a940fbdab 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt @@ -1,26 +1,39 @@ package com.zionhuang.music.ui.fragments +import android.app.SearchManager +import android.content.Intent import android.os.Bundle +import android.support.v4.media.MediaMetadataCompat +import android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE import android.support.v4.media.session.PlaybackStateCompat.* import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import com.google.android.material.bottomsheet.NeoBottomSheetBehavior.STATE_COLLAPSED import com.google.android.material.bottomsheet.NeoBottomSheetBehavior.STATE_HIDDEN +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.slider.Slider import com.zionhuang.innertube.models.BrowseEndpoint import com.zionhuang.innertube.models.BrowseLocalArtistSongsEndpoint import com.zionhuang.music.R +import com.zionhuang.music.constants.MediaConstants.EXTRA_MEDIA_METADATA import com.zionhuang.music.constants.MediaSessionConstants.ACTION_TOGGLE_LIKE import com.zionhuang.music.databinding.BottomControlsSheetBinding +import com.zionhuang.music.databinding.DialogEditLyricsBinding +import com.zionhuang.music.db.entities.LyricsEntity import com.zionhuang.music.db.entities.LyricsEntity.Companion.LYRICS_NOT_FOUND +import com.zionhuang.music.extensions.show import com.zionhuang.music.extensions.systemBarInsetsCompat import com.zionhuang.music.playback.MediaSessionConnection +import com.zionhuang.music.repos.SongRepository import com.zionhuang.music.ui.activities.MainActivity +import com.zionhuang.music.ui.fragments.dialogs.ChooseLyricsDialog import com.zionhuang.music.utils.NavigationEndpointHandler +import com.zionhuang.music.utils.lyrics.LyricsHelper import com.zionhuang.music.utils.makeTimeString import com.zionhuang.music.utils.preference.PreferenceLiveData import com.zionhuang.music.viewmodels.PlaybackViewModel @@ -45,6 +58,7 @@ class BottomControlsFragment : Fragment() { binding.lifecycleOwner = this binding.viewModel = viewModel + // Immersive layout binding.root.applyInsetter { type(navigationBars = true) { padding() @@ -107,6 +121,53 @@ class BottomControlsFragment : Fragment() { PreferenceLiveData(requireContext(), R.string.pref_lyrics_text_position, "1").observe(viewLifecycleOwner) { binding.lyricsView.setTextGravity(it.toIntOrNull() ?: 1) } + binding.btnLyricsAction.setOnClickListener { + MenuBottomSheetDialogFragment + .newInstance(R.menu.lyrics) + .setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.action_refetch -> lifecycleScope.launch { + MediaSessionConnection.binder?.songPlayer?.currentMediaMetadata?.value?.let { mediaMetadata -> + LyricsHelper.loadLyrics(requireContext(), mediaMetadata) + } + } + + R.id.action_edit -> { + val mediaId = viewModel.currentLyrics.value?.id ?: return@setOnMenuItemClickListener + val editLyricsBinding = DialogEditLyricsBinding.inflate(layoutInflater).apply { + textField.editText?.setText(viewModel.currentLyrics.value?.lyrics) + } + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.dialog_title_edit_lyrics) + .setView(editLyricsBinding.root) + .setPositiveButton(R.string.dialog_button_save) { _, _ -> + lifecycleScope.launch { + SongRepository(requireContext()).upsert(LyricsEntity( + mediaId, editLyricsBinding.textField.editText?.text.toString() + )) + } + } + .show() + } + R.id.action_search -> { + val mediaMetadata = viewModel.mediaMetadata.value ?: return@setOnMenuItemClickListener + try { + startActivity(Intent(Intent.ACTION_WEB_SEARCH).apply { + putExtra(SearchManager.QUERY, "${mediaMetadata.getString(METADATA_KEY_DISPLAY_SUBTITLE)} ${mediaMetadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE)} lyrics") + }) + } catch (_: Exception) { + } + } + R.id.action_choose -> { + val mediaMetadata = MediaSessionConnection.binder?.songPlayer?.currentMediaMetadata?.value ?: return@setOnMenuItemClickListener + ChooseLyricsDialog().apply { + arguments = bundleOf(EXTRA_MEDIA_METADATA to mediaMetadata) + }.show(requireContext()) + } + } + } + .show(requireContext()) + } lifecycleScope.launch { viewModel.playbackState.collect { playbackState -> if (playbackState.state != STATE_NONE && playbackState.state != STATE_STOPPED) { diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/ChooseLyricsDialog.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/ChooseLyricsDialog.kt new file mode 100644 index 000000000..df9b70210 --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/ChooseLyricsDialog.kt @@ -0,0 +1,65 @@ +package com.zionhuang.music.ui.fragments.dialogs + +import android.annotation.SuppressLint +import android.app.Dialog +import android.os.Bundle +import androidx.appcompat.app.AppCompatDialogFragment +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.zionhuang.music.R +import com.zionhuang.music.constants.MediaConstants.EXTRA_MEDIA_METADATA +import com.zionhuang.music.databinding.DialogChooseLyricsBinding +import com.zionhuang.music.db.entities.LyricsEntity +import com.zionhuang.music.extensions.addOnClickListener +import com.zionhuang.music.models.MediaMetadata +import com.zionhuang.music.repos.SongRepository +import com.zionhuang.music.ui.adapters.LyricsAdapter +import com.zionhuang.music.utils.lyrics.LyricsHelper +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +class ChooseLyricsDialog : AppCompatDialogFragment() { + private lateinit var binding: DialogChooseLyricsBinding + private lateinit var mediaMetadata: MediaMetadata + private val adapter = LyricsAdapter() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + mediaMetadata = arguments?.getParcelable(EXTRA_MEDIA_METADATA)!! + } + + @OptIn(DelicateCoroutinesApi::class) + @SuppressLint("NotifyDataSetChanged") + private fun setupUI() { + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + binding.recyclerView.addOnClickListener { position, _ -> + GlobalScope.launch { + SongRepository(requireContext()).upsert(LyricsEntity( + mediaMetadata.id, + adapter.items[position].lyrics + )) + } + dismiss() + } + lifecycleScope.launch { + adapter.items = LyricsHelper.getAllLyrics(requireContext(), mediaMetadata) + adapter.notifyDataSetChanged() + binding.progressBar.isVisible = false + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = DialogChooseLyricsBinding.inflate(layoutInflater) + setupUI() + + return MaterialAlertDialogBuilder(requireContext(), R.style.Dialog) + .setTitle(R.string.dialog_title_choose_lyrics) + .setView(binding.root) + .setNegativeButton(android.R.string.cancel, null) + .create() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/ui/viewholders/LyricsItemViewHolder.kt b/app/src/main/java/com/zionhuang/music/ui/viewholders/LyricsItemViewHolder.kt new file mode 100644 index 000000000..41b242445 --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/ui/viewholders/LyricsItemViewHolder.kt @@ -0,0 +1,28 @@ +package com.zionhuang.music.ui.viewholders + +import android.widget.TextView +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.zionhuang.music.databinding.ItemLyricsBinding +import com.zionhuang.music.extensions.context +import com.zionhuang.music.utils.lyrics.LyricsHelper + +class LyricsItemViewHolder( + val binding: ItemLyricsBinding, +) : RecyclerView.ViewHolder(binding.root) { + fun bind(lyricsResult: LyricsHelper.LyricsResult) { + binding.lyrics.text = lyricsResult.lyrics + binding.provider.text = lyricsResult.providerName + binding.synced.isVisible = lyricsResult.lyrics.startsWith("[") + binding.btnView.setOnClickListener { + MaterialAlertDialogBuilder(binding.context) + .setMessage(lyricsResult.lyrics) + .setPositiveButton(android.R.string.ok, null) + .show() + .apply { + window?.decorView?.findViewById(android.R.id.message)?.setTextIsSelectable(true) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsHelper.kt b/app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsHelper.kt new file mode 100644 index 000000000..b58affb52 --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsHelper.kt @@ -0,0 +1,63 @@ +package com.zionhuang.music.utils.lyrics + +import android.content.Context +import android.util.LruCache +import com.zionhuang.music.db.entities.LyricsEntity +import com.zionhuang.music.lyrics.KuGouLyricsProvider +import com.zionhuang.music.lyrics.YouTubeLyricsProvider +import com.zionhuang.music.models.MediaMetadata +import com.zionhuang.music.repos.SongRepository + +object LyricsHelper { + private val lyricsProviders = listOf(KuGouLyricsProvider, YouTubeLyricsProvider) + + private const val MAX_CACHE_SIZE = 10 + private val cache = LruCache>(MAX_CACHE_SIZE) + + suspend fun loadLyrics(context: Context, mediaMetadata: MediaMetadata) { + val songRepository = SongRepository(context) + val cached = cache.get(mediaMetadata.id)?.firstOrNull() + if (cached != null) { + songRepository.upsert(LyricsEntity(mediaMetadata.id, cached.lyrics)) + return + } + lyricsProviders.forEach { provider -> + if (provider.isEnabled(context)) { + provider.getLyrics( + mediaMetadata.id, + mediaMetadata.title, + mediaMetadata.artists.joinToString { it.name }, + mediaMetadata.duration + ).onSuccess { lyrics -> + songRepository.upsert(LyricsEntity(mediaMetadata.id, lyrics)) + return + }.onFailure { + it.printStackTrace() + } + } + } + songRepository.upsert(LyricsEntity(mediaMetadata.id, LyricsEntity.LYRICS_NOT_FOUND)) + } + + suspend fun getAllLyrics(context: Context, mediaMetadata: MediaMetadata): List = cache.get(mediaMetadata.id) ?: lyricsProviders.flatMap { provider -> + if (provider.isEnabled(context)) { + provider.getAllLyrics( + mediaMetadata.id, + mediaMetadata.title, + mediaMetadata.artists.joinToString { it.name }, + mediaMetadata.duration + ).getOrNull().orEmpty().map { + LyricsResult(provider.name, it) + } + } else { + emptyList() + } + }.also { + cache.put(mediaMetadata.id, it) + } + + data class LyricsResult( + val providerName: String, + val lyrics: String, + ) +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml index fac98d3b4..355fda7f3 100644 --- a/app/src/main/res/drawable/ic_search.xml +++ b/app/src/main/res/drawable/ic_search.xml @@ -1,9 +1,10 @@ + android:width="24dp" + android:height="24dp" + android:tint="?colorControlNormal" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:fillColor="@android:color/white" + android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" /> diff --git a/app/src/main/res/drawable/ic_sync.xml b/app/src/main/res/drawable/ic_sync.xml new file mode 100644 index 000000000..d4dba44ae --- /dev/null +++ b/app/src/main/res/drawable/ic_sync.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_visibility.xml b/app/src/main/res/drawable/ic_visibility.xml new file mode 100644 index 000000000..9cd541120 --- /dev/null +++ b/app/src/main/res/drawable/ic_visibility.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/dialog_choose_lyrics.xml b/app/src/main/res/layout/dialog_choose_lyrics.xml new file mode 100644 index 000000000..9bd84c1ad --- /dev/null +++ b/app/src/main/res/layout/dialog_choose_lyrics.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_edit_lyrics.xml b/app/src/main/res/layout/dialog_edit_lyrics.xml new file mode 100644 index 000000000..292dd870e --- /dev/null +++ b/app/src/main/res/layout/dialog_edit_lyrics.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_lyrics.xml b/app/src/main/res/layout/item_lyrics.xml new file mode 100644 index 000000000..6ad447a98 --- /dev/null +++ b/app/src/main/res/layout/item_lyrics.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/lyrics.xml b/app/src/main/res/menu/lyrics.xml new file mode 100644 index 000000000..31189b962 --- /dev/null +++ b/app/src/main/res/menu/lyrics.xml @@ -0,0 +1,19 @@ + +

+ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/search_and_settings.xml b/app/src/main/res/menu/search_and_settings.xml index b275222ff..a9a0489fd 100644 --- a/app/src/main/res/menu/search_and_settings.xml +++ b/app/src/main/res/menu/search_and_settings.xml @@ -7,7 +7,7 @@ android:id="@+id/action_search" android:icon="@drawable/ic_search" android:orderInCategory="300" - android:title="@string/menu_search_title" + android:title="@string/menu_search" app:actionViewClass="androidx.appcompat.widget.SearchView" app:showAsAction="ifRoom" /> \ No newline at end of file diff --git a/app/src/main/res/values-es-rUS/strings.xml b/app/src/main/res/values-es-rUS/strings.xml index 318c37ed6..15699e9d0 100644 --- a/app/src/main/res/values-es-rUS/strings.xml +++ b/app/src/main/res/values-es-rUS/strings.xml @@ -94,7 +94,7 @@ Gris azulado - Buscar + Buscar Search YouTube Music… Search library… @@ -119,6 +119,7 @@ Refetch Share Eliminar + Choose other lyrics Details @@ -132,6 +133,9 @@ File size Unknown + Edit lyrics + Choose lyrics + Editar canción Título Artista diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index e41f62993..18e89ce11 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -94,7 +94,7 @@ Gris azulado - Buscar + Buscar Search YouTube Music… Search library… @@ -119,6 +119,7 @@ Refetch Share Borrar + Choose other lyrics Details @@ -132,6 +133,9 @@ File size Unknown + Edit lyrics + Choose lyrics + Editar canción Título de la canción Artista de la canción diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index aa51f8c21..437bd192f 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -94,7 +94,7 @@ خاکستری آبی - جستجو + جستجو جستجو در یوتیوب موزیک… جستجوی کتاب‌خانه… @@ -119,6 +119,7 @@ نوسازی هم‌رسانی حذف + Choose other lyrics Details @@ -132,6 +133,9 @@ File size Unknown + Edit lyrics + Choose lyrics + ویرایش آهنگ عنوان آهنگ هنرمند آهنگ diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 5565c2b35..eca2bf429 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -94,7 +94,7 @@ Sinharmaa - Etsi + Etsi Search YouTube Music… Search library… @@ -119,6 +119,7 @@ Refetch Share Poista + Choose other lyrics Details @@ -132,6 +133,9 @@ File size Unknown + Edit lyrics + Choose lyrics + Muokkaa kappaletta Kappaleen nimi Artisti diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index b24d5f5b0..949dd9498 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -94,7 +94,7 @@ Bleu grisâtre - Recherche + Recherche Search YouTube Music… Search library… @@ -119,6 +119,7 @@ Refetch Share Effacer + Choose other lyrics Details @@ -132,6 +133,9 @@ File size Unknown + Edit lyrics + Choose lyrics + Editer la chanson Titre de la chanson Artiste de la chanson diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index da12a6a5b..20fd26c41 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -94,7 +94,7 @@ Kékesszürke - Keresés + Keresés YouTube Music keresés… Keresés könyvtárban… @@ -119,6 +119,7 @@ Újrahív Megosztás Törlés + Choose other lyrics Részletek @@ -132,6 +133,9 @@ Fájl méret Ismeretlen + Edit lyrics + Choose lyrics + Dal szerkesztése Dal címe Dal előadói diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 4fd079e2d..5209043bc 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -94,7 +94,7 @@ Blu grigio - Cerca + Cerca Cerca su YouTube Music… Cerca nella libreria… @@ -119,6 +119,7 @@ Riottieni Condividi Elimina + Choose other lyrics Dettagli @@ -132,6 +133,9 @@ Dimensioni Sconosciuto + Edit lyrics + Choose lyrics + Modifica brano Titolo Artista diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index d78e97566..29ac82eba 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -94,7 +94,7 @@ ブルーグレー - 検索 + 検索 YouTube Musicを検索… ライブラリを検索… @@ -119,6 +119,7 @@ 再取得 共有 削除 + Choose other lyrics Details @@ -132,6 +133,9 @@ File size Unknown + Edit lyrics + Choose lyrics + 曲を編集 曲名 曲のアーティスト diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 6dbcd9c17..4cf2f73ff 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -94,7 +94,7 @@ 블루 그레이 - 검색 + 검색 Search YouTube Music… Search library… @@ -119,6 +119,7 @@ Refetch Share 삭제 + Choose other lyrics Details @@ -132,6 +133,9 @@ File size Unknown + Edit lyrics + Choose lyrics + 노래 편집 노래 제목 노래 아티스트 diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml index 4360fe23e..544d11038 100644 --- a/app/src/main/res/values-ml-rIN/strings.xml +++ b/app/src/main/res/values-ml-rIN/strings.xml @@ -94,7 +94,7 @@ നീല ചാരനിറം - തിരയുക + തിരയുക യൂട്യൂബ് സംഗീതം തിരയുക… തിരയൽ ലൈബ്രറി… @@ -119,6 +119,7 @@ വീണ്ടെടുക്കുക പങ്കിടുക ഡിലീറ്റ് + Choose other lyrics Details @@ -132,6 +133,9 @@ File size Unknown + Edit lyrics + Choose lyrics + ഗാനം എഡിറ്റ് ചെയ്യുക പാട്ടിന്റെ പേര് പാട്ടുകാരൻ diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 6d2a02e62..6efd3528b 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -94,7 +94,7 @@ Azul-acinzentado - Pesquisar + Pesquisar Pesquisar no YouTube Music… Pesquisar na biblioteca… @@ -119,6 +119,7 @@ Atualizar Compartilhar Excluir + Choose other lyrics Detalhes @@ -132,6 +133,9 @@ Tamanho do arquivo Desconhecido + Edit lyrics + Choose lyrics + Editar música Título da Música Artista da Música diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index f6ad9ff78..9dbd4e9e4 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -94,7 +94,7 @@ Blågrå - Sök + Sök Search YouTube Music… Search library… @@ -119,6 +119,7 @@ Refetch Share Ta bort + Choose other lyrics Details @@ -132,6 +133,9 @@ File size Unknown + Edit lyrics + Choose lyrics + Redigera låten Låt titel Låt artist diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index cda3fc64c..cc7739699 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -94,7 +94,7 @@ 蓝灰 - 搜索 + 搜索 搜索 YouTube Music… 搜索媒体库… @@ -119,6 +119,7 @@ 刷新 分享 移除 + Choose other lyrics 详情 @@ -132,6 +133,9 @@ 文件大小 未知 + Edit lyrics + Choose lyrics + 编辑歌曲 歌名 音乐人 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 1ccbe8ba5..ef12c76b8 100755 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -94,7 +94,7 @@ 藍灰 - 搜尋 + 搜尋 搜尋 YouTube Music… 搜尋媒體庫… @@ -119,6 +119,7 @@ 更新資料 分享 移除 + 切換其他歌詞 詳細資訊 @@ -132,6 +133,9 @@ 檔案大小 未知 + 編輯歌詞 + 選擇歌詞 + 編輯歌曲 歌名 藝人 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c19a81470..c7c8680e9 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -94,7 +94,7 @@ Blue grey - Search + Search Search YouTube Music… Search library… @@ -119,6 +119,7 @@ Refetch Share Delete + Choose other lyrics Details @@ -132,6 +133,9 @@ File size Unknown + Edit lyrics + Choose lyrics + Edit song Song title Song artists diff --git a/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt b/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt index 609b6ab20..f57e01b0f 100644 --- a/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt +++ b/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt @@ -53,15 +53,27 @@ object KuGou { } suspend fun getLyrics(title: String, artist: String, duration: Int): Result = runCatching { - getLyricsCandidate(title, artist, duration)?.let { candidate -> - downloadLyrics(candidate.id, candidate.accesskey).content.decodeBase64String().normalize(normalizeTitle(title), normalizeArtist(artist)) + val keyword = generateKeyword(title, artist) + getLyricsCandidate(keyword, duration)?.let { candidate -> + downloadLyrics(candidate.id, candidate.accesskey).content.decodeBase64String().normalize(keyword) } ?: throw IllegalStateException("No lyrics candidate") } - suspend fun getLyricsCandidate(title: String, artist: String, duration: Int): SearchLyricsResponse.Candidate? { + suspend fun getAllLyrics(title: String, artist: String, duration: Int): Result> = runCatching { val keyword = generateKeyword(title, artist) + val candidates = searchSongs(keyword).data.info + .filter { abs(it.duration - duration) <= DURATION_TOLERANCE } + .mapNotNull { + searchLyricsByHash(it.hash).candidates.firstOrNull() + } + searchLyricsByKeyword(keyword, duration).candidates + candidates.map { + downloadLyrics(it.id, it.accesskey).content.decodeBase64String().normalize(keyword) + } + } + + suspend fun getLyricsCandidate(keyword: Pair, duration: Int): SearchLyricsResponse.Candidate? { searchSongs(keyword).data.info.forEach { song -> - if (abs(song.duration - duration) <= 8) { + if (abs(song.duration - duration) <= DURATION_TOLERANCE) { val candidate = searchLyricsByHash(song.hash).candidates.firstOrNull() if (candidate != null) return candidate } @@ -69,17 +81,17 @@ object KuGou { return searchLyricsByKeyword(keyword, duration).candidates.firstOrNull() } - private suspend fun searchSongs(keyword: String) = client.get("https://mobileservice.kugou.com/api/v3/search/song") { + private suspend fun searchSongs(keyword: Pair) = client.get("https://mobileservice.kugou.com/api/v3/search/song") { parameter("version", 9108) parameter("plat", 0) - parameter("keyword", keyword) + parameter("keyword", "${keyword.first} - ${keyword.second}") }.body() - private suspend fun searchLyricsByKeyword(keyword: String, duration: Int) = client.get("https://lyrics.kugou.com/search") { + private suspend fun searchLyricsByKeyword(keyword: Pair, duration: Int) = client.get("https://lyrics.kugou.com/search") { parameter("ver", 1) parameter("man", "yes") parameter("client", "pc") - parameter("keyword", keyword) + parameter("keyword", "${keyword.first} - ${keyword.second}") parameter("duration", duration * 1000) }.body() @@ -117,14 +129,12 @@ object KuGou { .replace("\\(.*\\)".toRegex(), "") .replace("(.*)".toRegex(), "") - private fun generateKeyword(title: String, artist: String): String { - return "${normalizeTitle(title)} - ${normalizeArtist(artist)}" - } + private fun generateKeyword(title: String, artist: String) = normalizeTitle(title) to normalizeArtist(artist) private fun String.toSimplifiedChinese() = ZhConverterUtil.toSimple(this) private fun String.toTraditionalChinese(useTraditionalChinese: Boolean) = if (useTraditionalChinese) ZhConverterUtil.toTraditional(this) else this - private fun String.normalize(title: String, artist: String): String = lines().filterNot { line -> + private fun String.normalize(keyword: Pair): String = lines().filterNot { line -> line.endsWith("]") }.let { var cutLine = 0 @@ -137,6 +147,7 @@ object KuGou { it.drop(cutLine) }.let { lines -> val firstLine = lines.firstOrNull()?.toSimplifiedChinese() ?: return@let lines + val (title, artist) = keyword if (title.toSimplifiedChinese() in firstLine || artist.split("、").any { it.toSimplifiedChinese() in firstLine @@ -154,4 +165,6 @@ object KuGou { UnicodeScript.HIRAGANA, UnicodeScript.KATAKANA, ) + + private const val DURATION_TOLERANCE = 8 } From 6c16722a64a2f4a720de1a8d653000f34f17857d Mon Sep 17 00:00:00 2001 From: Sdarfeesh <50188628+Sdarfeesh@users.noreply.github.com> Date: Sun, 30 Oct 2022 19:59:25 +0800 Subject: [PATCH 67/95] Update Simplified Chinese Translation --- app/src/main/res/values-zh-rCN/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index cc7739699..30d5cc4b8 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -119,7 +119,7 @@ 刷新 分享 移除 - Choose other lyrics + 切换其他歌词 详情 @@ -133,8 +133,8 @@ 文件大小 未知 - Edit lyrics - Choose lyrics + 编辑歌词 + 选择歌词 编辑歌曲 歌名 From addb7d457c7377e048ccc69bdfffe228d779c298 Mon Sep 17 00:00:00 2001 From: gidano Date: Sun, 30 Oct 2022 14:36:07 +0100 Subject: [PATCH 68/95] Update strings --- app/src/main/res/values-hu/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 20fd26c41..a0566cf43 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -99,7 +99,7 @@ Keresés könyvtárban… - Tetszett dalok + Lájkolt dalok Letöltött dalok @@ -119,7 +119,7 @@ Újrahív Megosztás Törlés - Choose other lyrics + Válasszon más dalszövegeket Részletek @@ -133,8 +133,8 @@ Fájl méret Ismeretlen - Edit lyrics - Choose lyrics + Dalszöveg szerkesztése + Válassza ki a dalszövegeket Dal szerkesztése Dal címe @@ -266,7 +266,7 @@ Tetszik - Tetszik eltávolítása + Lájk eltávolítása Könyvtárhoz ad Eltávolít a könyvtárból From 83d4a837f7ac017f2aaf98b62a832cbf51e5ec33 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sun, 30 Oct 2022 22:17:04 +0800 Subject: [PATCH 69/95] Optimize lyrics normalization --- .../main/java/com/zionhuang/kugou/KuGou.kt | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt b/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt index f57e01b0f..05d00a2b7 100644 --- a/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt +++ b/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt @@ -131,12 +131,10 @@ object KuGou { private fun generateKeyword(title: String, artist: String) = normalizeTitle(title) to normalizeArtist(artist) - private fun String.toSimplifiedChinese() = ZhConverterUtil.toSimple(this) - private fun String.toTraditionalChinese(useTraditionalChinese: Boolean) = if (useTraditionalChinese) ZhConverterUtil.toTraditional(this) else this - - private fun String.normalize(keyword: Pair): String = lines().filterNot { line -> - line.endsWith("]") + private fun String.normalize(keyword: Pair): String = lines().filter { line -> + line matches ACCEPTED_REGEX }.let { + // Remove useless information such as singer, writer, composer, guitar, etc. var cutLine = 0 for (i in min(30, it.lastIndex) downTo 0) { if (it[i] matches BANNED_REGEX) { @@ -149,16 +147,23 @@ object KuGou { val firstLine = lines.firstOrNull()?.toSimplifiedChinese() ?: return@let lines val (title, artist) = keyword if (title.toSimplifiedChinese() in firstLine || - artist.split("、").any { - it.toSimplifiedChinese() in firstLine - } + artist.split("、").any { it.toSimplifiedChinese() in firstLine } ) { lines.drop(1) } else lines - }.joinToString(separator = "\n").toTraditionalChinese(useTraditionalChinese && none { c -> - UnicodeScript.of(c.code) in JapaneseUnicodeScript - }) + }.joinToString(separator = "\n").let { + if (useTraditionalChinese) it.normalizeForTraditionalChinese() + else it + } + + private fun String.normalizeForTraditionalChinese() = + if (none { c -> UnicodeScript.of(c.code) in JapaneseUnicodeScript }) toTraditionalChinese() + else this + + private fun String.toSimplifiedChinese() = ZhConverterUtil.toSimple(this) + private fun String.toTraditionalChinese() = ZhConverterUtil.toTraditional(this) + private val ACCEPTED_REGEX = "\\[(\\d\\d):(\\d\\d)\\.(\\d{2,3})\\].*".toRegex() private val BANNED_REGEX = ".+].+[::].+".toRegex() private val JapaneseUnicodeScript = hashSetOf( From 420b585fd1569cd3ad73dfe2bf93279921f93724 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sun, 30 Oct 2022 22:26:47 +0800 Subject: [PATCH 70/95] Hide lyrics when playback state is error --- app/src/main/res/layout-land/bottom_controls_sheet.xml | 6 +++--- app/src/main/res/layout/bottom_controls_sheet.xml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/layout-land/bottom_controls_sheet.xml b/app/src/main/res/layout-land/bottom_controls_sheet.xml index d2f41933f..9f1d7ce1f 100644 --- a/app/src/main/res/layout-land/bottom_controls_sheet.xml +++ b/app/src/main/res/layout-land/bottom_controls_sheet.xml @@ -47,7 +47,7 @@ android:id="@+id/card_view" android:layout_width="0dp" android:layout_height="0dp" - android:visibility="@{viewModel.showLyrics ? View.GONE : View.VISIBLE}" + android:visibility="@{(viewModel.showLyrics && viewModel.playbackState.state != PlaybackState.STATE_ERROR) ? View.GONE : View.VISIBLE}" app:cardCornerRadius="@dimen/thumbnail_radius" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="1:1" @@ -137,7 +137,7 @@ android:fadingEdge="vertical" android:fadingEdgeLength="20dp" android:requiresFadingEdge="vertical" - android:visibility="@{(viewModel.showLyrics && viewModel.currentLyrics != null) ? View.VISIBLE : View.GONE}" + android:visibility="@{(viewModel.showLyrics && viewModel.currentLyrics != null && viewModel.playbackState.state != PlaybackState.STATE_ERROR) ? View.VISIBLE : View.GONE}" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" app:lrcCurrentTextColor="?colorPrimary" app:lrcLabel="@string/lyrics_not_found" @@ -163,7 +163,7 @@ android:focusable="true" android:padding="4dp" android:src="@drawable/ic_more_horiz" - android:visibility="@{viewModel.showLyrics ? View.VISIBLE : View.GONE}" + android:visibility="@{(viewModel.showLyrics && viewModel.playbackState.state != PlaybackState.STATE_ERROR) ? View.VISIBLE : View.GONE}" tools:ignore="SpeakableTextPresentCheck" /> diff --git a/app/src/main/res/layout/bottom_controls_sheet.xml b/app/src/main/res/layout/bottom_controls_sheet.xml index 8327a0726..5db1f4cbe 100644 --- a/app/src/main/res/layout/bottom_controls_sheet.xml +++ b/app/src/main/res/layout/bottom_controls_sheet.xml @@ -43,7 +43,7 @@ android:id="@+id/card_view" android:layout_width="0dp" android:layout_height="0dp" - android:visibility="@{viewModel.showLyrics ? View.GONE : View.VISIBLE}" + android:visibility="@{(viewModel.showLyrics && viewModel.playbackState.state != PlaybackState.STATE_ERROR) ? View.GONE : View.VISIBLE}" app:cardCornerRadius="@dimen/thumbnail_radius" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="1:1" @@ -135,7 +135,7 @@ android:fadingEdge="vertical" android:fadingEdgeLength="20dp" android:requiresFadingEdge="vertical" - android:visibility="@{(viewModel.showLyrics && viewModel.currentLyrics != null) ? View.VISIBLE : View.GONE}" + android:visibility="@{(viewModel.showLyrics && viewModel.currentLyrics != null && viewModel.playbackState.state != PlaybackState.STATE_ERROR) ? View.VISIBLE : View.GONE}" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" app:lrcCurrentTextColor="?colorPrimary" app:lrcLabel="@string/lyrics_not_found" @@ -162,7 +162,7 @@ android:focusable="true" android:padding="4dp" android:src="@drawable/ic_more_horiz" - android:visibility="@{viewModel.showLyrics ? View.VISIBLE : View.GONE}" + android:visibility="@{(viewModel.showLyrics && viewModel.playbackState.state != PlaybackState.STATE_ERROR) ? View.VISIBLE : View.GONE}" tools:ignore="SpeakableTextPresentCheck" /> From 41bf2238668eed27afd583142bde7b95d37874c1 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sun, 30 Oct 2022 22:52:08 +0800 Subject: [PATCH 71/95] Add missing settings icons --- app/src/main/res/drawable/ic_skip_next.xml | 4 ++-- app/src/main/res/drawable/ic_volume_up.xml | 11 +++++++++++ app/src/main/res/xml/pref_player_audio.xml | 2 ++ 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable/ic_volume_up.xml diff --git a/app/src/main/res/drawable/ic_skip_next.xml b/app/src/main/res/drawable/ic_skip_next.xml index 7b49ee498..993680b81 100644 --- a/app/src/main/res/drawable/ic_skip_next.xml +++ b/app/src/main/res/drawable/ic_skip_next.xml @@ -1,6 +1,6 @@ + + diff --git a/app/src/main/res/xml/pref_player_audio.xml b/app/src/main/res/xml/pref_player_audio.xml index cf6121666..a4444fa40 100644 --- a/app/src/main/res/xml/pref_player_audio.xml +++ b/app/src/main/res/xml/pref_player_audio.xml @@ -15,10 +15,12 @@ android:title="@string/pref_persistent_queue_title" /> Date: Sun, 30 Oct 2022 23:00:28 +0800 Subject: [PATCH 72/95] Option to hide buttons in player notification --- .../com/zionhuang/music/playback/SongPlayer.kt | 7 ++++++- app/src/main/res/drawable/ic_notifications.xml | 10 ++++++++++ app/src/main/res/values-es-rUS/strings.xml | 2 ++ app/src/main/res/values-es/strings.xml | 2 ++ app/src/main/res/values-fa-rIR/strings.xml | 2 ++ app/src/main/res/values-fi-rFI/strings.xml | 2 ++ app/src/main/res/values-fr-rFR/strings.xml | 2 ++ app/src/main/res/values-hu/strings.xml | 2 ++ app/src/main/res/values-it/strings.xml | 2 ++ app/src/main/res/values-ja-rJP/strings.xml | 2 ++ app/src/main/res/values-ko-rKR/strings.xml | 2 ++ app/src/main/res/values-ml-rIN/strings.xml | 2 ++ app/src/main/res/values-pt-rBR/strings.xml | 2 ++ app/src/main/res/values-sv-rSE/strings.xml | 2 ++ app/src/main/res/values-zh-rCN/strings.xml | 2 ++ app/src/main/res/values-zh-rTW/strings.xml | 2 ++ app/src/main/res/values/constants.xml | 1 + app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/xml/pref_general.xml | 18 ++++++++++++------ 19 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 app/src/main/res/drawable/ic_notifications.xml diff --git a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt index d05301e63..ecee98c03 100644 --- a/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt +++ b/app/src/main/java/com/zionhuang/music/playback/SongPlayer.kt @@ -334,7 +334,7 @@ class SongPlayer( override fun getCustomActions(player: Player): List { val actions = mutableListOf() - if (player.currentMetadata != null) { + if (player.currentMetadata != null && context.sharedPreferences.getBoolean(context.getString(R.string.pref_notification_more_action), true)) { actions.add(if (currentSong == null) ACTION_ADD_TO_LIBRARY else ACTION_REMOVE_FROM_LIBRARY) actions.add(if (currentSong?.song?.liked == true) ACTION_UNLIKE else ACTION_LIKE) } @@ -393,6 +393,11 @@ class SongPlayer( } } } + scope.launch { + context.sharedPreferences.booleanFlow(context.getString(R.string.pref_notification_more_action), true).collectLatest { + playerNotificationManager.invalidate() + } + } if (context.sharedPreferences.getBoolean(context.getString(R.string.pref_persistent_queue), true)) { runCatching { context.filesDir.resolve(PERSISTENT_QUEUE_FILE).inputStream().use { fis -> diff --git a/app/src/main/res/drawable/ic_notifications.xml b/app/src/main/res/drawable/ic_notifications.xml new file mode 100644 index 000000000..c5dc28d68 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values-es-rUS/strings.xml b/app/src/main/res/values-es-rUS/strings.xml index 15699e9d0..6edfd3ef3 100644 --- a/app/src/main/res/values-es-rUS/strings.xml +++ b/app/src/main/res/values-es-rUS/strings.xml @@ -58,6 +58,8 @@ Añadir automáticamente la canción a la biblioteca Añadir canción a la biblioteca cuando termine de reproducirse Expandir reproductor inferior cuando comience la reproducción + More actions in notification + Show add to library and like buttons Privacy Pause search history diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 18e89ce11..f0938c87e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -58,6 +58,8 @@ Agregar canción automáticamente a la biblioteca Agregue una canción a su biblioteca cuando termine de reproducirse Expandir reproductor inferior al reproducir + More actions in notification + Show add to library and like buttons Privacy Pause search history diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index 437bd192f..6d5a81cc5 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -58,6 +58,8 @@ افزودن خودکار آهنگ به کتاب‌خانه افزودن آهنگ به کتاب‌خانه پس از اتمام پخش گسترش پخش‌کننده‌ی پایین هنگام پخش + More actions in notification + Show add to library and like buttons حریم‌خصوصی متوقف‌کردن تاریخچه جستجو diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index eca2bf429..d67793198 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -58,6 +58,8 @@ Automaattinen vienti kirjastoon Lisää kappale kirjastoon automaattisesti, kun se on soitettu Laajenna alapalkki toistaessa + More actions in notification + Show add to library and like buttons Privacy Pause search history diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 949dd9498..ad842aa1c 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -58,6 +58,8 @@ Ajout automatique de chansons à la bibliothèque Add song to your library when it completes playing Ajouter la chanson à votre bibliothèque lorsqu\'elle est terminée. + More actions in notification + Show add to library and like buttons Privacy Pause search history diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index a0566cf43..2d6c92e17 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -58,6 +58,8 @@ Dal auto. hozzáadása a könyvtárhoz Adja hozzá a dalt a könyvtárához, amikor a lejátszás befejeződött Bontsa ki az alsó lejátszót lejátszás közben + More actions in notification + Show add to library and like buttons Adatvédelem Keresési előzmények szüneteltetése diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 5209043bc..eaac49236 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -58,6 +58,8 @@ Aggiunta automatica nella libreria Aggiungi la canzone nella tua libreria quando viene terminato l\'ascolto Espandi il riproduttore in basso quando inizia un ascolto + More actions in notification + Show add to library and like buttons Privacy Sospendi la cronologia delle ricerche diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 29ac82eba..b84e80312 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -58,6 +58,8 @@ 曲を自動でライブラリに追加 再生が終了したら曲をライブラリに追加する 再生時にボトムプレイヤーを展開する + More actions in notification + Show add to library and like buttons プライバシー 履歴の記録を一時停止 diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 4cf2f73ff..fa52182d9 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -58,6 +58,8 @@ 라이브러리에 노래 자동 추가 재생이 완료되면 라이브러리에 노래 추가 플레이 시 하단 플레이어 확장 + More actions in notification + Show add to library and like buttons Privacy Pause search history diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml index 544d11038..add397221 100644 --- a/app/src/main/res/values-ml-rIN/strings.xml +++ b/app/src/main/res/values-ml-rIN/strings.xml @@ -58,6 +58,8 @@ ലൈബ്രറിയിലേക്ക് പാട്ട് സ്വയമേവ ചേർക്കുക നിങ്ങളുടെ ലൈബ്രറി പ്ലേ ചെയ്യുമ്പോൾ പാട്ട് ചേർക്കുക താഴെയുള്ള പ്ലേയർ വികസിപ്പിക്കുക + More actions in notification + Show add to library and like buttons സ്വകാര്യത തിരയൽ ചരിത്രം താൽക്കാലികമായി നിർത്തുക diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 6efd3528b..7155aa2f7 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -58,6 +58,8 @@ Adicionar música à biblioteca automaticamente Adicionar música à sua biblioteca quando terminar de ser reproduzida Expandir player ao reproduzir + More actions in notification + Show add to library and like buttons Privacidade Pausar histórico de pesquisa diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 9dbd4e9e4..65f85fa30 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -58,6 +58,8 @@ Lägg till låt i biblioteket automatiskt Lägg till låten i biblioteket efter det spelats klart Utöka bottenspelaren vid låtspel + More actions in notification + Show add to library and like buttons Privacy Pause search history diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 30d5cc4b8..cf4663504 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -58,6 +58,8 @@ 自动将音乐添加到媒体库 播放结束时添加到媒体库 播放音乐时展开播放器 + More actions in notification + Show add to library and like buttons 隐私 暂停搜索记录 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index ef12c76b8..f016cc4d5 100755 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -58,6 +58,8 @@ 自動將音樂加入媒體庫 在結束播放時加入你的音樂 在點擊音樂時展開播放器 + 在通知顯示更多按鈕 + 顯示「加入媒體庫」和「喜歡」按鈕 隱私 暫停搜尋紀錄 diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index b4272f7bf..5a3c64880 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -38,6 +38,7 @@ AUTO_DOWNLOAD AUTO_ADD_SONG EXPAND_ON_PLAY + NOTIFICATION_MORE_ACTION EQUALIZER OPEN_SAF diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c7c8680e9..cfc84d5e4 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -58,6 +58,8 @@ Auto add song to library Add song to your library when it completes playing Expand bottom player on play + More actions in notification + Show add to library and like buttons Privacy Pause search history diff --git a/app/src/main/res/xml/pref_general.xml b/app/src/main/res/xml/pref_general.xml index e0408cc8c..d0f921569 100644 --- a/app/src/main/res/xml/pref_general.xml +++ b/app/src/main/res/xml/pref_general.xml @@ -1,21 +1,27 @@ - + + \ No newline at end of file From 8e06035a367e1492aeea91298b09245d2e0b86d6 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Sun, 30 Oct 2022 23:36:04 +0800 Subject: [PATCH 73/95] Update readme and bug report form --- .github/ISSUE_TEMPLATE/bug_report.yml | 6 +++--- README.md | 12 +++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 0cc94dcec..c63a41354 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,13 +1,13 @@ name: Bug report description: Create a bug report to help us improve -labels: [bug] +labels: [ bug ] body: - type: checkboxes id: checklist attributes: label: Checklist options: - - label: I am able to reproduce the bug with the [latest version](https://github.com/z-huang/InnerTune/releases/latest). + - label: I am able to reproduce the bug with the [latest DEBUG version](https://github.com/z-huang/InnerTune/actions). required: true - label: I've checked that there is no issue about this bug. required: true @@ -65,7 +65,7 @@ body: label: Logs description: | If your bug includes a crash, please use `adb logcat` or other ways to provide logs. - + - type: input id: music-version attributes: diff --git a/README.md b/README.md index b145fb748..5d8fb6535 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,17 @@ You can install _InnerTune_ using the following methods: How to get updates? 1. F-Droid application. -2. [GitHub](https://github.com/z-huang/InnerTune) +2. [GitHub](https://github.com/z-huang/InnerTune) + [Obtainium (beta)](https://github.com/ImranR98/Obtainium) + +## FAQ + +### Q: How to scrobble music to LastFM, LibreFM, ListenBrainz or GNU FM? + +Use other music scrobbler apps. I recommend [Pano Scrobbler](https://play.google.com/store/apps/details?id=com.arn.scrobble). + +### Q: How to export downloaded song files? + +*InnerTune* support SAF. You can find the provider in Android native file manager. You can also use [Material Files](https://play.google.com/store/apps/details?id=me.zhanghai.android.files) with [instruction](https://github.com/z-huang/InnerTune/issues/117#issuecomment-1295090708) (recommended). ## Contribution From 722f8cf574a03879e6ca9ac387ba49dfa3b49a43 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Mon, 31 Oct 2022 12:20:21 +0800 Subject: [PATCH 74/95] Improve zh-TW lyrics normalization --- kugou/src/main/java/com/zionhuang/kugou/KuGou.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt b/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt index 05d00a2b7..73b522d16 100644 --- a/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt +++ b/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt @@ -158,6 +158,8 @@ object KuGou { private fun String.normalizeForTraditionalChinese() = if (none { c -> UnicodeScript.of(c.code) in JapaneseUnicodeScript }) toTraditionalChinese() + .replace('着', '著') + .replace('羣', '群') else this private fun String.toSimplifiedChinese() = ZhConverterUtil.toSimple(this) From 30de5ef1d67668cff4d25fb5646ab80d22000b42 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Mon, 31 Oct 2022 12:39:53 +0800 Subject: [PATCH 75/95] Fix #375 --- app/proguard-rules.pro | 3 -- app/src/main/java/com/zionhuang/music/App.kt | 16 ++++++-- .../music/viewmodels/SuggestionViewModel.kt | 21 +++++++++- build.gradle.kts | 1 - innertube/build.gradle.kts | 39 ------------------- .../java/com/zionhuang/innertube/InnerTube.kt | 2 +- .../java/com/zionhuang/innertube/YouTube.kt | 13 +++++++ .../innertube/models/YouTubeClient.kt | 22 ----------- innertube/src/main/proto/visitorData.proto | 9 ----- .../com/zionhuang/innertube/YouTubeTest.kt | 15 +------ 10 files changed, 47 insertions(+), 94 deletions(-) delete mode 100644 innertube/src/main/proto/visitorData.proto diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 2204ffa7d..1aa730e53 100755 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -80,9 +80,6 @@ # @Serializable and @Polymorphic are used at runtime for polymorphic serialization. -keepattributes RuntimeVisibleAnnotations,AnnotationDefault -# Protobuf --keep class * extends com.google.protobuf.GeneratedMessageLite { *; } - -dontwarn javax.servlet.ServletContainerInitializer -dontwarn org.bouncycastle.jsse.BCSSLParameters -dontwarn org.bouncycastle.jsse.BCSSLSocket diff --git a/app/src/main/java/com/zionhuang/music/App.kt b/app/src/main/java/com/zionhuang/music/App.kt index 90bb72cac..ba5f31344 100644 --- a/app/src/main/java/com/zionhuang/music/App.kt +++ b/app/src/main/java/com/zionhuang/music/App.kt @@ -10,17 +10,20 @@ import coil.ImageLoader import coil.ImageLoaderFactory import coil.disk.DiskCache import com.zionhuang.innertube.YouTube -import com.zionhuang.innertube.models.YouTubeClient import com.zionhuang.innertube.models.YouTubeLocale import com.zionhuang.kugou.KuGou import com.zionhuang.music.extensions.getEnum import com.zionhuang.music.extensions.sharedPreferences import com.zionhuang.music.extensions.toInetSocketAddress import com.zionhuang.music.ui.fragments.settings.StorageSettingsFragment.Companion.VALUE_TO_MB +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import java.net.Proxy import java.util.* class App : Application(), ImageLoaderFactory { + @OptIn(DelicateCoroutinesApi::class) override fun onCreate() { super.onCreate() INSTANCE = this @@ -57,9 +60,14 @@ class App : Application(), ImageLoaderFactory { } } - YouTube.visitorData = sharedPreferences.getString(getString(R.string.pref_visitor_data), null) ?: YouTubeClient.generateVisitorData().also { - sharedPreferences.edit { - putString(getString(R.string.pref_visitor_data), it) + GlobalScope.launch { + val visitorData = sharedPreferences.getString(getString(R.string.pref_visitor_data), null) ?: YouTube.generateVisitorData().getOrNull()?.also { + sharedPreferences.edit { + putString(getString(R.string.pref_visitor_data), it) + } + } + visitorData?.let { + YouTube.visitorData = it } } } diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/SuggestionViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/SuggestionViewModel.kt index d6adc4deb..90fedf667 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/SuggestionViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/SuggestionViewModel.kt @@ -1,32 +1,51 @@ package com.zionhuang.music.viewmodels import android.app.Application +import androidx.core.content.edit import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.zionhuang.innertube.YouTube import com.zionhuang.innertube.models.SuggestionTextItem import com.zionhuang.innertube.models.SuggestionTextItem.SuggestionSource.LOCAL import com.zionhuang.innertube.models.YTBaseItem +import com.zionhuang.music.R +import com.zionhuang.music.extensions.sharedPreferences import com.zionhuang.music.repos.SongRepository import com.zionhuang.music.repos.YouTubeRepository import kotlinx.coroutines.launch +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.MissingFieldException class SuggestionViewModel(application: Application) : AndroidViewModel(application) { private val songRepository = SongRepository(application) private val youTubeRepository = YouTubeRepository(application) val suggestions = MutableLiveData>(emptyList()) + @OptIn(ExperimentalSerializationApi::class) fun fetchSuggestions(query: String?) = viewModelScope.launch { if (query.isNullOrEmpty()) { suggestions.postValue(songRepository.getAllSearchHistory().map { SuggestionTextItem(it.query, LOCAL) }) } else { try { - val history = songRepository.getSearchHistory(query).map { SuggestionTextItem(it.query, LOCAL) } + val history = songRepository.getSearchHistory(query).map { + SuggestionTextItem(it.query, LOCAL) + } suggestions.postValue(history + youTubeRepository.getSuggestions(query).filter { item -> item !is SuggestionTextItem || history.find { it.query == item.query } == null }) } catch (e: Exception) { e.printStackTrace() + // Remove in future + if (e is MissingFieldException) { + // Reset visitorData + YouTube.generateVisitorData().getOrNull()?.let { + getApplication().sharedPreferences.edit { + putString(getApplication().getString(R.string.pref_visitor_data), it) + } + YouTube.visitorData = it + } + } } } } diff --git a/build.gradle.kts b/build.gradle.kts index a843d20b5..1336c46d3 100755 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,6 @@ buildscript { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") classpath("org.jetbrains.kotlin:kotlin-serialization:$kotlin_version") classpath("dev.rikka.tools.materialthemebuilder:gradle-plugin:1.3.2") - classpath("com.google.protobuf:protobuf-gradle-plugin:0.8.19") // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/innertube/build.gradle.kts b/innertube/build.gradle.kts index 191bc84e6..9be847736 100644 --- a/innertube/build.gradle.kts +++ b/innertube/build.gradle.kts @@ -1,15 +1,8 @@ -import com.android.build.api.dsl.AndroidSourceSet -import com.google.protobuf.gradle.builtins -import com.google.protobuf.gradle.generateProtoTasks -import com.google.protobuf.gradle.protobuf -import com.google.protobuf.gradle.protoc - plugins { id("com.android.library") id("org.jetbrains.kotlin.android") id("kotlinx-serialization") id("kotlin-parcelize") - id("com.google.protobuf") } android { @@ -34,29 +27,6 @@ android { kotlinOptions { jvmTarget = "1.8" } - sourceSets { - getByName("main") { - proto { - srcDir("src/main/proto") - } - } - } -} - - -protobuf { - protoc { - artifact = "com.google.protobuf:protoc:3.21.7" - } - generateProtoTasks { - all().forEach { task -> - task.builtins { - create("java") { - option("lite") - } - } - } - } } tasks.withType().configureEach { @@ -75,14 +45,5 @@ dependencies { implementation("ch.qos.logback:logback-classic:$logback_version") implementation("io.ktor:ktor-client-logging-jvm:$ktor_version") implementation("org.brotli:dec:0.1.2") - implementation("com.google.protobuf:protobuf-javalite:3.21.7") testImplementation("junit:junit:4.13.2") -} - -fun AndroidSourceSet.proto(action: SourceDirectorySet.() -> Unit) { - (this as? ExtensionAware) - ?.extensions - ?.getByName("proto") - ?.let { it as? SourceDirectorySet } - ?.apply(action) } \ No newline at end of file diff --git a/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt b/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt index facbbd4a4..5e6a51fb7 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt @@ -22,7 +22,7 @@ import java.util.* * For making HTTP requests, not parsing response. */ class InnerTube { - private var httpClient = createClient() + var httpClient = createClient() var locale = YouTubeLocale( gl = Locale.getDefault().country, diff --git a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt index d617bef2c..3549e2645 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt @@ -6,7 +6,12 @@ import com.zionhuang.innertube.models.YouTubeClient.Companion.WEB_REMIX import com.zionhuang.innertube.models.response.* import com.zionhuang.innertube.utils.insertSeparator import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.client.statement.* import io.ktor.http.* +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive import java.net.Proxy /** @@ -150,6 +155,14 @@ object YouTube { .mapNotNull { it.content.playlistPanelVideoRenderer?.toSongItem() } } + suspend fun generateVisitorData() = runCatching { + Json.parseToJsonElement(innerTube.httpClient.get("https://music.youtube.com/sw.js_data").bodyAsText().substring(5)) + .jsonArray[0] + .jsonArray[2] + .jsonArray[6] + .jsonPrimitive.content + } + @JvmInline value class SearchFilter(val value: String) { companion object { diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/YouTubeClient.kt b/innertube/src/main/java/com/zionhuang/innertube/models/YouTubeClient.kt index b66053139..873e4ad4e 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/models/YouTubeClient.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/models/YouTubeClient.kt @@ -1,6 +1,5 @@ package com.zionhuang.innertube.models -import android.util.Base64 import kotlinx.serialization.Serializable @Serializable @@ -22,27 +21,6 @@ data class YouTubeClient( ) companion object { - fun generateVisitorData(): String { - val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toList() - var id = "" - for (i in 1..11) { - id += alphabet.random() - } - val timestamp = System.currentTimeMillis() / 1000 - return generateVisitorData(id, timestamp.toInt()) - } - - fun generateVisitorData(id: String, timestamp: Int): String { - val visitorData = VisitorData.visitorData.newBuilder() - .setId(id) - .setTimestamp(timestamp) - .build() - return Base64.encodeToString(visitorData.toByteArray(), Base64.NO_WRAP) - .replace("==", "%3D%3D") - .replace("+", "-") - .replace("/", "_") - } - private const val REFERER_YOUTUBE_MUSIC = "https://music.youtube.com/" private const val USER_AGENT_WEB = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36" diff --git a/innertube/src/main/proto/visitorData.proto b/innertube/src/main/proto/visitorData.proto deleted file mode 100644 index d5ce03c07..000000000 --- a/innertube/src/main/proto/visitorData.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto2"; - -option java_package = "com.zionhuang.innertube.models"; -option java_outer_classname = "VisitorData"; - -message visitorData { - required string id = 1; - required int32 timestamp = 5; -} \ No newline at end of file diff --git a/innertube/src/test/java/com/zionhuang/innertube/YouTubeTest.kt b/innertube/src/test/java/com/zionhuang/innertube/YouTubeTest.kt index 0b21fa783..6d52b40e5 100644 --- a/innertube/src/test/java/com/zionhuang/innertube/YouTubeTest.kt +++ b/innertube/src/test/java/com/zionhuang/innertube/YouTubeTest.kt @@ -11,7 +11,6 @@ import com.zionhuang.innertube.YouTube.SearchFilter.Companion.FILTER_VIDEO import com.zionhuang.innertube.models.BrowseEndpoint import com.zionhuang.innertube.models.WatchEndpoint import com.zionhuang.innertube.models.YTItem -import com.zionhuang.innertube.models.YouTubeClient import com.zionhuang.innertube.utils.browseAll import io.ktor.client.* import io.ktor.client.engine.okhttp.* @@ -20,10 +19,9 @@ import io.ktor.http.* import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue -import org.junit.Ignore import org.junit.Test -@Ignore("IDK Why GitHub Action always runs the test with error") +//@Ignore("IDK Why GitHub Action always runs the test with error") class YouTubeTest { private val youTube = YouTube @@ -174,17 +172,6 @@ class YouTubeTest { assertTrue(browseResult.lyrics != null) } - @Test - fun visitorData() { - assertEquals("CgtsZG1ySnZiQWtSbyiMjuGSBg%3D%3D", YouTubeClient.generateVisitorData("ldmrJvbAkRo", 1649952524)) - assertEquals("CgtrLTlNQlQ2SWs1OCjZlu-ZBg%3D%3D", YouTubeClient.generateVisitorData("k-9MBT6Ik58", 1664863065)) - assertEquals("Cgs1bFZLWXJWVk5MRSisnO-ZBg%3D%3D", YouTubeClient.generateVisitorData("5lVKYrVVNLE", 1664863788)) - assertEquals("CgtrLTlNQlQ2SWs1OCjN_fSZBg%3D%3D", YouTubeClient.generateVisitorData("k-9MBT6Ik58", 1664958157)) - assertEquals("CgtXSHBibzJXSm1layj-ifWZBg%3D%3D", YouTubeClient.generateVisitorData("WHpbo2WJmek", 1664959742)) - assertEquals("CgtlcWs4cjFPYUpyZyj_ifWZBg%3D%3D", YouTubeClient.generateVisitorData("eqk8r1OaJrg", 1664959743)) - assertEquals("Cgs2eHNHQ3FTVkJDbyj-i_WZBg%3D%3D", YouTubeClient.generateVisitorData("6xsGCqSVBCo", 1664959998)) - } - companion object { private val VIDEO_IDS = listOf( "4H-N260cPCg", From 603aab98e62347d12462ecd65917451721feffa1 Mon Sep 17 00:00:00 2001 From: Sdarfeesh <50188628+Sdarfeesh@users.noreply.github.com> Date: Mon, 31 Oct 2022 13:01:09 +0800 Subject: [PATCH 76/95] Update Simplified Chinese Translation --- app/src/main/res/values-zh-rCN/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index cf4663504..d9978b538 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -58,8 +58,8 @@ 自动将音乐添加到媒体库 播放结束时添加到媒体库 播放音乐时展开播放器 - More actions in notification - Show add to library and like buttons + 在通知显示更多按钮 + 显示“添加到媒体库”和“喜欢”按钮 隐私 暂停搜索记录 @@ -154,7 +154,7 @@ 音乐人名称不能为空 重复的音乐人 - 音乐人 %1$s 已存在。 + 音乐人“%1$s”已存在。 选择播放列表 @@ -252,7 +252,7 @@ 已添加到媒体库 已从媒体库中移除 已导入此播放列表 - 已添加到 %1$s + 已添加到“%1$s” 正在下载此歌曲 正在下载此 %d 首歌曲 From 5ef9662a3a8d39224a8155b78f10d20e380f6852 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Mon, 31 Oct 2022 13:17:47 +0800 Subject: [PATCH 77/95] Ignore YouTubeTest --- innertube/src/test/java/com/zionhuang/innertube/YouTubeTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/innertube/src/test/java/com/zionhuang/innertube/YouTubeTest.kt b/innertube/src/test/java/com/zionhuang/innertube/YouTubeTest.kt index 6d52b40e5..2cc91bd88 100644 --- a/innertube/src/test/java/com/zionhuang/innertube/YouTubeTest.kt +++ b/innertube/src/test/java/com/zionhuang/innertube/YouTubeTest.kt @@ -19,9 +19,10 @@ import io.ktor.http.* import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test -//@Ignore("IDK Why GitHub Action always runs the test with error") +@Ignore("IDK Why GitHub Action always runs the test with error") class YouTubeTest { private val youTube = YouTube From 22551e345918daa2831999faac70d86472577327 Mon Sep 17 00:00:00 2001 From: gidano Date: Mon, 31 Oct 2022 10:26:28 +0100 Subject: [PATCH 78/95] Updated 10.31 --- app/src/main/res/values-hu/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 2d6c92e17..927248625 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -58,8 +58,8 @@ Dal auto. hozzáadása a könyvtárhoz Adja hozzá a dalt a könyvtárához, amikor a lejátszás befejeződött Bontsa ki az alsó lejátszót lejátszás közben - More actions in notification - Show add to library and like buttons + További műveletek az értesítésben + Könyvtárhoz adás és tetszés gombok megjelenítése Adatvédelem Keresési előzmények szüneteltetése @@ -89,7 +89,7 @@ Világoszöld Lime Sárga - Amber + Borostyán Narancs Sötét narancs Barna From 40af231e1f56b1b40aa97583f427229b7b28f8a9 Mon Sep 17 00:00:00 2001 From: Michael Moroni Date: Mon, 31 Oct 2022 13:39:37 +0100 Subject: [PATCH 79/95] Update strings.xml Updated Italian translation --- app/src/main/res/values-it/strings.xml | 34 +++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index eaac49236..5affcd4ef 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -20,10 +20,10 @@ Segui sistema Scheda principale predefinita Personalizza schede di navigazione - Lyrics text position - Left - Center - Right + Posizione del testo dei brani + Sinistra + Centro + Destra Contenuti Lingua predefinita dei contenuti @@ -39,13 +39,13 @@ Alta Bassa Coda persistente - Skip silence - Audio normalization + Salta silenzio + Normalizzazione dell\'audio Equalizzatore - Storage - View downloaded files in SAF - This may not work in some devices + Archiviazione + Guarda file scaricati in SAF + Potrebbe non funzionare su alcuni dispositivi Cache Grandezza massima della cache delle immagini Pulisci cache delle immagini @@ -58,14 +58,14 @@ Aggiunta automatica nella libreria Aggiungi la canzone nella tua libreria quando viene terminato l\'ascolto Espandi il riproduttore in basso quando inizia un ascolto - More actions in notification - Show add to library and like buttons + Ulteriori azioni in notifica + Mostra i bottoni Aggiungi alla libreria e Mi piace Privacy Sospendi la cronologia delle ricerche Pulisci la cronologia delle ricerche Sei sicuro di voler cancellare la cronologia delle ricerche? - Enable KuGou lyrics provider + Attiva i testi forniti da KuGou Backup e ripristina Backup @@ -121,7 +121,7 @@ Riottieni Condividi Elimina - Choose other lyrics + Scegli un altro testo Dettagli @@ -130,13 +130,13 @@ Codec Bitrate Frequenza di campionamento - Volume + Rumorosità Volume Dimensioni Sconosciuto - Edit lyrics - Choose lyrics + Modifica testo + Scegli testo Modifica brano Titolo @@ -300,5 +300,5 @@ Brani cercati - Lyrics not found + Testo non trovato From 3ea38434489b632acf0461ab061a4e828d7dfc59 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Mon, 31 Oct 2022 22:32:54 +0800 Subject: [PATCH 80/95] Fix #381 --- app/src/main/java/com/zionhuang/music/repos/SongRepository.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt index a0ce15276..747af8290 100644 --- a/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt +++ b/app/src/main/java/com/zionhuang/music/repos/SongRepository.kt @@ -542,6 +542,7 @@ class SongRepository(private val context: Context) : LocalRepository { val songs = songDao.getAlbumSongs(album.id).map { it.copy(album = null) } albumDao.delete(album.album) deleteSongs(songs) + artistDao.delete(album.artists.filter { artistDao.getArtistSongCount(it.id) == 0 }) } } From f96a675d25e230de6cede4041d06aa3628d6f57c Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 1 Nov 2022 13:45:36 +0800 Subject: [PATCH 81/95] Add search lyrics dialog and improve player layout --- .../music/lyrics/KuGouLyricsProvider.kt | 4 +- .../zionhuang/music/lyrics/LyricsProvider.kt | 4 +- .../music/lyrics/YouTubeLyricsProvider.kt | 6 +-- .../music/ui/activities/MainActivity.kt | 2 +- .../ui/fragments/BottomControlsFragment.kt | 8 +-- .../fragments/dialogs/ChooseLyricsDialog.kt | 44 ++++++++++++---- .../fragments/dialogs/SearchLyricsDialog.kt | 50 +++++++++++++++++++ .../music/utils/lyrics/LyricsHelper.kt | 24 ++++----- .../res/layout-land/bottom_controls_sheet.xml | 8 +-- .../main/res/layout/bottom_controls_sheet.xml | 2 +- .../main/res/layout/dialog_search_lyrics.xml | 44 ++++++++++++++++ app/src/main/res/values-es-rUS/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fa-rIR/strings.xml | 1 + app/src/main/res/values-fi-rFI/strings.xml | 1 + app/src/main/res/values-fr-rFR/strings.xml | 1 + app/src/main/res/values-hu/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-ja-rJP/strings.xml | 1 + app/src/main/res/values-ko-rKR/strings.xml | 1 + app/src/main/res/values-ml-rIN/strings.xml | 1 + app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-sv-rSE/strings.xml | 1 + app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../main/java/com/zionhuang/kugou/KuGou.kt | 6 +-- 27 files changed, 172 insertions(+), 45 deletions(-) create mode 100644 app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/SearchLyricsDialog.kt create mode 100644 app/src/main/res/layout/dialog_search_lyrics.xml diff --git a/app/src/main/java/com/zionhuang/music/lyrics/KuGouLyricsProvider.kt b/app/src/main/java/com/zionhuang/music/lyrics/KuGouLyricsProvider.kt index e18c8e0a9..dfe68eb15 100644 --- a/app/src/main/java/com/zionhuang/music/lyrics/KuGouLyricsProvider.kt +++ b/app/src/main/java/com/zionhuang/music/lyrics/KuGouLyricsProvider.kt @@ -10,9 +10,9 @@ object KuGouLyricsProvider : LyricsProvider { override fun isEnabled(context: Context): Boolean = context.sharedPreferences.getBoolean(context.getString(R.string.pref_enable_kugou), true) - override suspend fun getLyrics(id: String, title: String, artist: String, duration: Int): Result = + override suspend fun getLyrics(id: String?, title: String, artist: String, duration: Int): Result = KuGou.getLyrics(title, artist, duration) - override suspend fun getAllLyrics(id: String, title: String, artist: String, duration: Int): Result> = + override suspend fun getAllLyrics(id: String?, title: String, artist: String, duration: Int): Result> = KuGou.getAllLyrics(title, artist, duration) } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/lyrics/LyricsProvider.kt b/app/src/main/java/com/zionhuang/music/lyrics/LyricsProvider.kt index 4ed49c917..30210fac7 100644 --- a/app/src/main/java/com/zionhuang/music/lyrics/LyricsProvider.kt +++ b/app/src/main/java/com/zionhuang/music/lyrics/LyricsProvider.kt @@ -5,6 +5,6 @@ import android.content.Context interface LyricsProvider { val name: String fun isEnabled(context: Context): Boolean - suspend fun getLyrics(id: String, title: String, artist: String, duration: Int): Result - suspend fun getAllLyrics(id: String, title: String, artist: String, duration: Int): Result> + suspend fun getLyrics(id: String?, title: String, artist: String, duration: Int): Result + suspend fun getAllLyrics(id: String?, title: String, artist: String, duration: Int): Result> } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/lyrics/YouTubeLyricsProvider.kt b/app/src/main/java/com/zionhuang/music/lyrics/YouTubeLyricsProvider.kt index 56fc049bf..6daddf107 100644 --- a/app/src/main/java/com/zionhuang/music/lyrics/YouTubeLyricsProvider.kt +++ b/app/src/main/java/com/zionhuang/music/lyrics/YouTubeLyricsProvider.kt @@ -7,13 +7,13 @@ import com.zionhuang.innertube.models.WatchEndpoint object YouTubeLyricsProvider : LyricsProvider { override val name = "YouTube Music" override fun isEnabled(context: Context) = true - override suspend fun getLyrics(id: String, title: String, artist: String, duration: Int): Result = - YouTube.next(WatchEndpoint(videoId = id)).mapCatching { nextResult -> + override suspend fun getLyrics(id: String?, title: String, artist: String, duration: Int): Result = + YouTube.next(WatchEndpoint(videoId = id!!)).mapCatching { nextResult -> YouTube.browse(nextResult.lyricsEndpoint ?: throw IllegalStateException("Lyrics endpoint not found")).getOrThrow() }.mapCatching { browseResult -> browseResult.lyrics ?: throw IllegalStateException("Lyrics unavailable") } - override suspend fun getAllLyrics(id: String, title: String, artist: String, duration: Int): Result> = + override suspend fun getAllLyrics(id: String?, title: String, artist: String, duration: Int): Result> = getLyrics(id, title, artist, duration).map { listOf(it) } } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt b/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt index 8555e7b76..39bc0bd51 100644 --- a/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt +++ b/app/src/main/java/com/zionhuang/music/ui/activities/MainActivity.kt @@ -165,7 +165,7 @@ class MainActivity : ThemedBindingActivity(), NavController lifecycleScope.launch { AdaptiveUtils.orientation.collectLatest { orientation -> binding.queueSheet.updateLayoutParams { - width = if (orientation == ORIENTATION_LANDSCAPE) (resources.displayMetrics.widthPixels * 0.6).toInt() else resources.displayMetrics.widthPixels + width = if (orientation == ORIENTATION_LANDSCAPE) (resources.displayMetrics.widthPixels * 0.5).toInt() else resources.displayMetrics.widthPixels } binding.container.updateLayoutParams { bottomMargin = if (orientation == ORIENTATION_PORTRAIT) resources.getDimensionPixelSize(R.dimen.m3_bottom_nav_min_height) else 0 diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt index a940fbdab..b92ae2dec 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/BottomControlsFragment.kt @@ -9,7 +9,6 @@ import android.support.v4.media.session.PlaybackStateCompat.* import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope @@ -20,7 +19,6 @@ import com.google.android.material.slider.Slider import com.zionhuang.innertube.models.BrowseEndpoint import com.zionhuang.innertube.models.BrowseLocalArtistSongsEndpoint import com.zionhuang.music.R -import com.zionhuang.music.constants.MediaConstants.EXTRA_MEDIA_METADATA import com.zionhuang.music.constants.MediaSessionConstants.ACTION_TOGGLE_LIKE import com.zionhuang.music.databinding.BottomControlsSheetBinding import com.zionhuang.music.databinding.DialogEditLyricsBinding @@ -31,7 +29,7 @@ import com.zionhuang.music.extensions.systemBarInsetsCompat import com.zionhuang.music.playback.MediaSessionConnection import com.zionhuang.music.repos.SongRepository import com.zionhuang.music.ui.activities.MainActivity -import com.zionhuang.music.ui.fragments.dialogs.ChooseLyricsDialog +import com.zionhuang.music.ui.fragments.dialogs.SearchLyricsDialog import com.zionhuang.music.utils.NavigationEndpointHandler import com.zionhuang.music.utils.lyrics.LyricsHelper import com.zionhuang.music.utils.makeTimeString @@ -160,9 +158,7 @@ class BottomControlsFragment : Fragment() { } R.id.action_choose -> { val mediaMetadata = MediaSessionConnection.binder?.songPlayer?.currentMediaMetadata?.value ?: return@setOnMenuItemClickListener - ChooseLyricsDialog().apply { - arguments = bundleOf(EXTRA_MEDIA_METADATA to mediaMetadata) - }.show(requireContext()) + SearchLyricsDialog(mediaMetadata).show(requireContext()) } } } diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/ChooseLyricsDialog.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/ChooseLyricsDialog.kt index df9b70210..6f988ace0 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/ChooseLyricsDialog.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/ChooseLyricsDialog.kt @@ -4,16 +4,15 @@ import android.annotation.SuppressLint import android.app.Dialog import android.os.Bundle import androidx.appcompat.app.AppCompatDialogFragment +import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.zionhuang.music.R -import com.zionhuang.music.constants.MediaConstants.EXTRA_MEDIA_METADATA import com.zionhuang.music.databinding.DialogChooseLyricsBinding import com.zionhuang.music.db.entities.LyricsEntity import com.zionhuang.music.extensions.addOnClickListener -import com.zionhuang.music.models.MediaMetadata import com.zionhuang.music.repos.SongRepository import com.zionhuang.music.ui.adapters.LyricsAdapter import com.zionhuang.music.utils.lyrics.LyricsHelper @@ -21,14 +20,30 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -class ChooseLyricsDialog : AppCompatDialogFragment() { +class ChooseLyricsDialog() : AppCompatDialogFragment() { private lateinit var binding: DialogChooseLyricsBinding - private lateinit var mediaMetadata: MediaMetadata private val adapter = LyricsAdapter() + private var mediaId: String? = null + private lateinit var songTitle: String + private lateinit var songArtists: String + private var duration = -1 + + constructor(mediaId: String?, songTitle: String, songArtists: String, duration: Int) : this() { + arguments = bundleOf( + EXTRA_MEDIA_ID to mediaId, + EXTRA_SONG_TITLE to songTitle, + EXTRA_ARTISTS to songArtists, + EXTRA_DURATION to duration + ) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - mediaMetadata = arguments?.getParcelable(EXTRA_MEDIA_METADATA)!! + mediaId = arguments?.getString(EXTRA_MEDIA_ID) + songTitle = arguments?.getString(EXTRA_SONG_TITLE)!! + songArtists = arguments?.getString(EXTRA_ARTISTS)!! + duration = arguments?.getInt(EXTRA_DURATION) ?: -1 } @OptIn(DelicateCoroutinesApi::class) @@ -38,15 +53,17 @@ class ChooseLyricsDialog : AppCompatDialogFragment() { binding.recyclerView.adapter = adapter binding.recyclerView.addOnClickListener { position, _ -> GlobalScope.launch { - SongRepository(requireContext()).upsert(LyricsEntity( - mediaMetadata.id, - adapter.items[position].lyrics - )) + mediaId?.let { mediaId -> + SongRepository(requireContext()).upsert(LyricsEntity( + mediaId, + adapter.items[position].lyrics + )) + } } dismiss() } lifecycleScope.launch { - adapter.items = LyricsHelper.getAllLyrics(requireContext(), mediaMetadata) + adapter.items = LyricsHelper.getAllLyrics(requireContext(), mediaId, songTitle, songArtists, duration) adapter.notifyDataSetChanged() binding.progressBar.isVisible = false } @@ -62,4 +79,11 @@ class ChooseLyricsDialog : AppCompatDialogFragment() { .setNegativeButton(android.R.string.cancel, null) .create() } + + companion object { + const val EXTRA_MEDIA_ID = "media_id" + const val EXTRA_SONG_TITLE = "song_title" + const val EXTRA_ARTISTS = "artists" + const val EXTRA_DURATION = "duration" + } } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/SearchLyricsDialog.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/SearchLyricsDialog.kt new file mode 100644 index 000000000..0cdc1db30 --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/SearchLyricsDialog.kt @@ -0,0 +1,50 @@ +package com.zionhuang.music.ui.fragments.dialogs + +import android.app.Dialog +import android.os.Bundle +import androidx.appcompat.app.AppCompatDialogFragment +import androidx.core.os.bundleOf +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.zionhuang.music.R +import com.zionhuang.music.constants.MediaConstants.EXTRA_MEDIA_METADATA +import com.zionhuang.music.databinding.DialogSearchLyricsBinding +import com.zionhuang.music.models.MediaMetadata + +class SearchLyricsDialog() : AppCompatDialogFragment() { + private lateinit var binding: DialogSearchLyricsBinding + private var mediaMetadata: MediaMetadata? = null + + constructor(mediaMetadata: MediaMetadata) : this() { + arguments = bundleOf(EXTRA_MEDIA_METADATA to mediaMetadata) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + mediaMetadata = arguments?.getParcelable(EXTRA_MEDIA_METADATA) + } + + private fun setupUI() { + binding.songTitle.editText?.setText(mediaMetadata?.title.orEmpty()) + binding.songArtist.editText?.setText(mediaMetadata?.artists?.joinToString { it.name }.orEmpty()) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = DialogSearchLyricsBinding.inflate(layoutInflater) + setupUI() + + return MaterialAlertDialogBuilder(requireContext(), R.style.Dialog) + .setTitle(R.string.dialog_title_search_lyrics) + .setView(binding.root) + .setPositiveButton(android.R.string.ok) { _, _ -> + ChooseLyricsDialog( + mediaMetadata?.id, + binding.songTitle.editText?.text.toString(), + binding.songArtist.editText?.text.toString(), + mediaMetadata?.duration ?: -1 + ).show(parentFragmentManager, null) + dismiss() + } + .setNegativeButton(android.R.string.cancel, null) + .create() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsHelper.kt b/app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsHelper.kt index b58affb52..ec4ee5046 100644 --- a/app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsHelper.kt +++ b/app/src/main/java/com/zionhuang/music/utils/lyrics/LyricsHelper.kt @@ -39,21 +39,19 @@ object LyricsHelper { songRepository.upsert(LyricsEntity(mediaMetadata.id, LyricsEntity.LYRICS_NOT_FOUND)) } - suspend fun getAllLyrics(context: Context, mediaMetadata: MediaMetadata): List = cache.get(mediaMetadata.id) ?: lyricsProviders.flatMap { provider -> - if (provider.isEnabled(context)) { - provider.getAllLyrics( - mediaMetadata.id, - mediaMetadata.title, - mediaMetadata.artists.joinToString { it.name }, - mediaMetadata.duration - ).getOrNull().orEmpty().map { - LyricsResult(provider.name, it) + suspend fun getAllLyrics(context: Context, mediaId: String?, songTitle: String, songArtists: String, duration: Int): List { + val cacheKey = "$songArtists-$songTitle".replace(" ", "") + return cache.get(cacheKey) ?: lyricsProviders.flatMap { provider -> + if (provider.isEnabled(context)) { + provider.getAllLyrics(mediaId, songTitle, songArtists, duration).getOrNull().orEmpty().map { + LyricsResult(provider.name, it) + } + } else { + emptyList() } - } else { - emptyList() + }.also { + cache.put(cacheKey, it) } - }.also { - cache.put(mediaMetadata.id, it) } data class LyricsResult( diff --git a/app/src/main/res/layout-land/bottom_controls_sheet.xml b/app/src/main/res/layout-land/bottom_controls_sheet.xml index 9f1d7ce1f..531dcde46 100644 --- a/app/src/main/res/layout-land/bottom_controls_sheet.xml +++ b/app/src/main/res/layout-land/bottom_controls_sheet.xml @@ -31,7 +31,7 @@ @@ -143,7 +143,7 @@ app:lrcLabel="@string/lyrics_not_found" app:lrcNormalTextColor="?android:textColorHint" app:lrcNormalTextSize="20sp" - app:lrcPadding="16dp" + app:lrcPadding="28dp" app:lrcTextGravity="center" app:lrcTextSize="24sp" app:lrcTimelineColor="#00000000" @@ -172,15 +172,15 @@ android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginBottom="64dp" - android:layout_weight="6" + android:layout_weight="1" android:paddingStart="8dp" + android:paddingTop="32dp" android:paddingEnd="16dp"> + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-es-rUS/strings.xml b/app/src/main/res/values-es-rUS/strings.xml index 6edfd3ef3..49802e160 100644 --- a/app/src/main/res/values-es-rUS/strings.xml +++ b/app/src/main/res/values-es-rUS/strings.xml @@ -136,6 +136,7 @@ Unknown Edit lyrics + Search lyrics Choose lyrics Editar canción diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index f0938c87e..0c1b00132 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -136,6 +136,7 @@ Unknown Edit lyrics + Search lyrics Choose lyrics Editar canción diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index 6d5a81cc5..abab51b64 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -136,6 +136,7 @@ Unknown Edit lyrics + Search lyrics Choose lyrics ویرایش آهنگ diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index d67793198..0a710e8c0 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -136,6 +136,7 @@ Unknown Edit lyrics + Search lyrics Choose lyrics Muokkaa kappaletta diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index ad842aa1c..7adb33154 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -136,6 +136,7 @@ Unknown Edit lyrics + Search lyrics Choose lyrics Editer la chanson diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 927248625..a182c988a 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -136,6 +136,7 @@ Ismeretlen Dalszöveg szerkesztése + Search lyrics Válassza ki a dalszövegeket Dal szerkesztése diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 5affcd4ef..45b11edaf 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -136,6 +136,7 @@ Sconosciuto Modifica testo + Search lyrics Scegli testo Modifica brano diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index b84e80312..166fe32d6 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -136,6 +136,7 @@ Unknown Edit lyrics + Search lyrics Choose lyrics 曲を編集 diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index fa52182d9..d18cd7159 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -136,6 +136,7 @@ Unknown Edit lyrics + Search lyrics Choose lyrics 노래 편집 diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml index add397221..7382c6f9c 100644 --- a/app/src/main/res/values-ml-rIN/strings.xml +++ b/app/src/main/res/values-ml-rIN/strings.xml @@ -136,6 +136,7 @@ Unknown Edit lyrics + Search lyrics Choose lyrics ഗാനം എഡിറ്റ് ചെയ്യുക diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 7155aa2f7..97b65ac22 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -136,6 +136,7 @@ Desconhecido Edit lyrics + Search lyrics Choose lyrics Editar música diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 65f85fa30..a684881a6 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -136,6 +136,7 @@ Unknown Edit lyrics + Search lyrics Choose lyrics Redigera låten diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index d9978b538..87f44e105 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -136,6 +136,7 @@ 未知 编辑歌词 + 搜索歌词 选择歌词 编辑歌曲 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index f016cc4d5..09065c206 100755 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -136,6 +136,7 @@ 未知 編輯歌詞 + 搜尋歌詞 選擇歌詞 編輯歌曲 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cfc84d5e4..3915f05a0 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -136,6 +136,7 @@ Unknown Edit lyrics + Search lyrics Choose lyrics Edit song diff --git a/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt b/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt index 73b522d16..4ec3cdec3 100644 --- a/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt +++ b/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt @@ -62,7 +62,7 @@ object KuGou { suspend fun getAllLyrics(title: String, artist: String, duration: Int): Result> = runCatching { val keyword = generateKeyword(title, artist) val candidates = searchSongs(keyword).data.info - .filter { abs(it.duration - duration) <= DURATION_TOLERANCE } + .filter { duration == -1 || abs(it.duration - duration) <= DURATION_TOLERANCE } // if duration == -1, we don't care duration .mapNotNull { searchLyricsByHash(it.hash).candidates.firstOrNull() } + searchLyricsByKeyword(keyword, duration).candidates @@ -73,7 +73,7 @@ object KuGou { suspend fun getLyricsCandidate(keyword: Pair, duration: Int): SearchLyricsResponse.Candidate? { searchSongs(keyword).data.info.forEach { song -> - if (abs(song.duration - duration) <= DURATION_TOLERANCE) { + if (duration == -1 || abs(song.duration - duration) <= DURATION_TOLERANCE) { // if duration == -1, we don't care duration val candidate = searchLyricsByHash(song.hash).candidates.firstOrNull() if (candidate != null) return candidate } @@ -92,7 +92,7 @@ object KuGou { parameter("man", "yes") parameter("client", "pc") parameter("keyword", "${keyword.first} - ${keyword.second}") - parameter("duration", duration * 1000) + parameter("duration", duration.takeIf { it != -1 }?.times(1000)) // if duration == -1, we don't care duration }.body() private suspend fun searchLyricsByHash(hash: String) = client.get("https://lyrics.kugou.com/search") { From c4199990d45d445202aac648e258c209478b13e5 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 1 Nov 2022 13:58:02 +0800 Subject: [PATCH 82/95] Remove useless line in the end of lyrics --- kugou/src/main/java/com/zionhuang/kugou/KuGou.kt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt b/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt index 4ec3cdec3..6a5268875 100644 --- a/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt +++ b/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt @@ -135,14 +135,23 @@ object KuGou { line matches ACCEPTED_REGEX }.let { // Remove useless information such as singer, writer, composer, guitar, etc. - var cutLine = 0 + var headCutLine = 0 for (i in min(30, it.lastIndex) downTo 0) { if (it[i] matches BANNED_REGEX) { - cutLine = i + 1 + headCutLine = i + 1 break } } - it.drop(cutLine) + it.drop(headCutLine) + }.let { + var tailCutLine = 0 + for (i in min(it.size - 30, it.lastIndex) downTo 0) { + if (it[it.lastIndex - i] matches BANNED_REGEX) { + tailCutLine = i + 1 + break + } + } + it.dropLast(tailCutLine) }.let { lines -> val firstLine = lines.firstOrNull()?.toSimplifiedChinese() ?: return@let lines val (title, artist) = keyword From 7007fc3df902b9e3a5586287bbc2dd8f3fd27e8b Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 1 Nov 2022 14:08:57 +0800 Subject: [PATCH 83/95] Show itag in stats for nerds --- .../ui/fragments/dialogs/SongDetailsDialog.kt | 3 +- .../main/res/layout/dialog_song_details.xml | 398 ++++++++++-------- 2 files changed, 213 insertions(+), 188 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/SongDetailsDialog.kt b/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/SongDetailsDialog.kt index 4136516c9..cc850d634 100644 --- a/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/SongDetailsDialog.kt +++ b/app/src/main/java/com/zionhuang/music/ui/fragments/dialogs/SongDetailsDialog.kt @@ -44,12 +44,13 @@ class SongDetailsDialog : AppCompatDialogFragment() { } lifecycleScope.launch { viewModel.currentSongFormat.collectLatest { format -> + binding.itag.text = format?.itag?.toString() ?: getString(R.string.unknown) binding.mimeType.text = format?.mimeType ?: getString(R.string.unknown) binding.codecs.text = format?.codecs ?: getString(R.string.unknown) binding.bitrate.text = format?.bitrate?.let { "${it / 1000} Kbps" } ?: getString(R.string.unknown) binding.sampleRate.text = format?.sampleRate?.let { "$it Hz" } ?: getString(R.string.unknown) binding.loudness.text = format?.loudnessDb?.let { "$it dB" } ?: getString(R.string.unknown) - binding.fileSize.text = format?.contentLength?.let { Formatter.formatShortFileSize(requireContext(), it) } + binding.fileSize.text = format?.contentLength?.let { Formatter.formatShortFileSize(requireContext(), it) } ?: getString(R.string.unknown) } } lifecycleScope.launch { diff --git a/app/src/main/res/layout/dialog_song_details.xml b/app/src/main/res/layout/dialog_song_details.xml index 2eb0c1327..fc1582347 100644 --- a/app/src/main/res/layout/dialog_song_details.xml +++ b/app/src/main/res/layout/dialog_song_details.xml @@ -2,192 +2,216 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="wrap_content"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From a9bcbaa9af65efa5cf58840a1cca718a0a057599 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 1 Nov 2022 15:00:54 +0800 Subject: [PATCH 84/95] Change lyrics menu text --- app/src/main/res/menu/lyrics.xml | 2 +- app/src/main/res/values-es-rUS/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fa-rIR/strings.xml | 1 + app/src/main/res/values-fi-rFI/strings.xml | 1 + app/src/main/res/values-fr-rFR/strings.xml | 1 + app/src/main/res/values-hu/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-ja-rJP/strings.xml | 1 + app/src/main/res/values-ko-rKR/strings.xml | 1 + app/src/main/res/values-ml-rIN/strings.xml | 1 + app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-sv-rSE/strings.xml | 1 + app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 16 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/menu/lyrics.xml b/app/src/main/res/menu/lyrics.xml index 31189b962..aa3000dec 100644 --- a/app/src/main/res/menu/lyrics.xml +++ b/app/src/main/res/menu/lyrics.xml @@ -11,7 +11,7 @@ + android:title="@string/menu_search_online" /> Refetch Share Eliminar + Search online Choose other lyrics diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 0c1b00132..697f7bb53 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -121,6 +121,7 @@ Refetch Share Borrar + Search online Choose other lyrics diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index abab51b64..91e20af97 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -121,6 +121,7 @@ نوسازی هم‌رسانی حذف + Search online Choose other lyrics diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 0a710e8c0..69eea1e76 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -121,6 +121,7 @@ Refetch Share Poista + Search online Choose other lyrics diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 7adb33154..2b4237d39 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -121,6 +121,7 @@ Refetch Share Effacer + Search online Choose other lyrics diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index a182c988a..13aa6d506 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -121,6 +121,7 @@ Újrahív Megosztás Törlés + Search online Válasszon más dalszövegeket diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 45b11edaf..bef84c949 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -121,6 +121,7 @@ Riottieni Condividi Elimina + Search online Scegli un altro testo diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 166fe32d6..9076f9f92 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -121,6 +121,7 @@ 再取得 共有 削除 + Search online Choose other lyrics diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index d18cd7159..0eb0a529b 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -121,6 +121,7 @@ Refetch Share 삭제 + Search online Choose other lyrics diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml index 7382c6f9c..f8edcba75 100644 --- a/app/src/main/res/values-ml-rIN/strings.xml +++ b/app/src/main/res/values-ml-rIN/strings.xml @@ -121,6 +121,7 @@ വീണ്ടെടുക്കുക പങ്കിടുക ഡിലീറ്റ് + Search online Choose other lyrics diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 97b65ac22..3751e9caf 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -121,6 +121,7 @@ Atualizar Compartilhar Excluir + Search online Choose other lyrics diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index a684881a6..7b29b8866 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -121,6 +121,7 @@ Refetch Share Ta bort + Search online Choose other lyrics diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 87f44e105..c25983fa4 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -121,6 +121,7 @@ 刷新 分享 移除 + Search online 切换其他歌词 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 09065c206..14d1c21e3 100755 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -121,6 +121,7 @@ 更新資料 分享 移除 + 線上搜尋 切換其他歌詞 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3915f05a0..1864dbd94 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -121,6 +121,7 @@ Refetch Share Delete + Search online Choose other lyrics From 02ab32250627a998ff70f2789db2730249d8b8db Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 1 Nov 2022 15:01:38 +0800 Subject: [PATCH 85/95] Temporarily disable regenerate visitorData (#375) --- .../music/viewmodels/SuggestionViewModel.kt | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/SuggestionViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/SuggestionViewModel.kt index 90fedf667..f957378b2 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/SuggestionViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/SuggestionViewModel.kt @@ -1,21 +1,16 @@ package com.zionhuang.music.viewmodels import android.app.Application -import androidx.core.content.edit import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import com.zionhuang.innertube.YouTube import com.zionhuang.innertube.models.SuggestionTextItem import com.zionhuang.innertube.models.SuggestionTextItem.SuggestionSource.LOCAL import com.zionhuang.innertube.models.YTBaseItem -import com.zionhuang.music.R -import com.zionhuang.music.extensions.sharedPreferences import com.zionhuang.music.repos.SongRepository import com.zionhuang.music.repos.YouTubeRepository import kotlinx.coroutines.launch import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.MissingFieldException class SuggestionViewModel(application: Application) : AndroidViewModel(application) { private val songRepository = SongRepository(application) @@ -36,16 +31,17 @@ class SuggestionViewModel(application: Application) : AndroidViewModel(applicati }) } catch (e: Exception) { e.printStackTrace() - // Remove in future - if (e is MissingFieldException) { - // Reset visitorData - YouTube.generateVisitorData().getOrNull()?.let { - getApplication().sharedPreferences.edit { - putString(getApplication().getString(R.string.pref_visitor_data), it) - } - YouTube.visitorData = it - } - } + // Fix incorrect visitorData + // comment out because now YouTube Music doesn't give us suggestions if we're not logged in +// if (e is MissingFieldException) { +// // Reset visitorData +// YouTube.generateVisitorData().getOrNull()?.let { +// getApplication().sharedPreferences.edit { +// putString(getApplication().getString(R.string.pref_visitor_data), it) +// } +// YouTube.visitorData = it +// } +// } } } } From 397152dc22b9848f0c241c3d7198631f16725037 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 1 Nov 2022 15:04:31 +0800 Subject: [PATCH 86/95] Bump version to 0.4.3 --- app/build.gradle.kts | 4 +-- .../metadata/android/en-US/changelogs/14.txt | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/14.txt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cca23c800..9d1ecfb8d 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,8 +15,8 @@ android { applicationId = "com.zionhuang.music" minSdk = 24 targetSdk = 32 - versionCode = 13 - versionName = "0.4.2" + versionCode = 14 + versionName = "0.4.3" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" javaCompileOptions { annotationProcessorOptions { diff --git a/fastlane/metadata/android/en-US/changelogs/14.txt b/fastlane/metadata/android/en-US/changelogs/14.txt new file mode 100644 index 000000000..17a24b807 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/14.txt @@ -0,0 +1,34 @@ +New + +- Lyrics #76 +- Support Android Auto #125 +- Skip silence +- Audio normalization #24 +- Export downloaded songs via SAF #117 + +Improvement + +- Improve player view layout +- Add wake lock for player +- Option to hide buttons in player notification +- Delete persistent queue when restoring database +- Show itag in stats for nerds + +Fixed + +- Fix crash in cache settings #315 +- Fix can't play downloaded songs #329 +- Fix missing field error in InnerTube #334 +- Fix display error when removing current song in queue #323 +- Fix cached image being redownloaded +- Fix ActivityNotFoundException in voice search #362 +- Fix hardware bitmap exception #360 +- Fix artists with no song may not be deleted automatically #381 + +Translation + +- Add Hungarian translation +- Update Brazilian Portuguese translation +- Update Simplified Chinese translation +- Update Hungarian translation +- Update Italian translation \ No newline at end of file From 9163e246082f75ae91c5c44fcbca96efb001452a Mon Sep 17 00:00:00 2001 From: Michael Moroni Date: Tue, 1 Nov 2022 10:34:34 +0100 Subject: [PATCH 87/95] Create 14.txt --- .../metadata/android/it/changelogs/14.txt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 fastlane/metadata/android/it/changelogs/14.txt diff --git a/fastlane/metadata/android/it/changelogs/14.txt b/fastlane/metadata/android/it/changelogs/14.txt new file mode 100644 index 000000000..9914e72bd --- /dev/null +++ b/fastlane/metadata/android/it/changelogs/14.txt @@ -0,0 +1,34 @@ +Novità + +- Testi dei brani #76 +- Supporto ad Android Auto #125 +- Salta il silenzio +- Normalizzazione dell'audio #24 +- Esporta brani scaricati via SAF #117 + +Miglioramenti + +- Migliorato il formato del lettore +- Aggiunto blocco sveglia per il lettore +- Opzione per nascondere i bottoni nella notifica +- Elimina la coda persistente quando si ripristina il database +- Mostra itag nelle statistiche per nerd + +Sistemato + +- Sistemata la chiusura inaspettata dell'app nelle impostazioni della cache #315 +- Sistemata l'impossibilità di riprodurre i brani scaricati #329 +- Sistemato l'errore dei campi mancanti in InnerTube #334 +- Sistemato errore che veniva mostrato quando si rimuoveva il brano corrente dalla coda #323 +- Sistemato il riscaricamento delle immagini nella cache +- Sistemato ActivityNotFoundException nella ricerca vocale #362 +- Sistemato eccezione di hardware bitmap #360 +- Sistemata l'eliminazione automatica degli artisti privi di brani #381 + +Aggiornamenti della traduzione + +- Aggiunta traduzione in ungherese +- Aggiornata traduzione in portoghese brasiliano +- Aggiornata traduzione in cinese semplificato +- Aggiornata traduzione in ungherese +- Aggiornata traduzione in italiano From c297f70570a0f34e6180a85879ec9209ef37f7c1 Mon Sep 17 00:00:00 2001 From: Michael Moroni Date: Tue, 1 Nov 2022 10:41:22 +0100 Subject: [PATCH 88/95] Update strings.xml --- app/src/main/res/values-it/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index bef84c949..2f39585ec 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -121,7 +121,7 @@ Riottieni Condividi Elimina - Search online + Cerca online Scegli un altro testo @@ -137,7 +137,7 @@ Sconosciuto Modifica testo - Search lyrics + Cerca testo Scegli testo Modifica brano From 7cc3ec55f286868f3b567e8694a8a3d73e3d0fcf Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 1 Nov 2022 19:57:47 +0800 Subject: [PATCH 89/95] Always show history search queries --- .../music/viewmodels/SuggestionViewModel.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/SuggestionViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/SuggestionViewModel.kt index f957378b2..2705ad114 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/SuggestionViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/SuggestionViewModel.kt @@ -10,25 +10,23 @@ import com.zionhuang.innertube.models.YTBaseItem import com.zionhuang.music.repos.SongRepository import com.zionhuang.music.repos.YouTubeRepository import kotlinx.coroutines.launch -import kotlinx.serialization.ExperimentalSerializationApi class SuggestionViewModel(application: Application) : AndroidViewModel(application) { private val songRepository = SongRepository(application) private val youTubeRepository = YouTubeRepository(application) val suggestions = MutableLiveData>(emptyList()) - @OptIn(ExperimentalSerializationApi::class) fun fetchSuggestions(query: String?) = viewModelScope.launch { if (query.isNullOrEmpty()) { suggestions.postValue(songRepository.getAllSearchHistory().map { SuggestionTextItem(it.query, LOCAL) }) } else { - try { - val history = songRepository.getSearchHistory(query).map { - SuggestionTextItem(it.query, LOCAL) - } - suggestions.postValue(history + youTubeRepository.getSuggestions(query).filter { item -> + val history = songRepository.getSearchHistory(query).map { + SuggestionTextItem(it.query, LOCAL) + } + val ytSuggestions = try { + youTubeRepository.getSuggestions(query).filter { item -> item !is SuggestionTextItem || history.find { it.query == item.query } == null - }) + } } catch (e: Exception) { e.printStackTrace() // Fix incorrect visitorData @@ -42,7 +40,9 @@ class SuggestionViewModel(application: Application) : AndroidViewModel(applicati // YouTube.visitorData = it // } // } + emptyList() } + suggestions.postValue(history + ytSuggestions) } } } \ No newline at end of file From 73b051df9bf093962775a5e0c6124cc3cf67a4b8 Mon Sep 17 00:00:00 2001 From: Sdarfeesh <50188628+Sdarfeesh@users.noreply.github.com> Date: Tue, 1 Nov 2022 19:57:49 +0800 Subject: [PATCH 90/95] Update Simplified Chinese Translation 0.4.3 --- app/src/main/res/values-zh-rCN/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index c25983fa4..1255a10c7 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -121,7 +121,7 @@ 刷新 分享 移除 - Search online + 在线搜索 切换其他歌词 From 7142111439b5cf9d3c1a3ad46e7f28a44d977cd8 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 1 Nov 2022 20:50:55 +0800 Subject: [PATCH 91/95] Maybe fix #386 --- .../zionhuang/music/ui/viewholders/YouTubeViewHolder.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/ui/viewholders/YouTubeViewHolder.kt b/app/src/main/java/com/zionhuang/music/ui/viewholders/YouTubeViewHolder.kt index d2c553c90..a3d07c2ea 100644 --- a/app/src/main/java/com/zionhuang/music/ui/viewholders/YouTubeViewHolder.kt +++ b/app/src/main/java/com/zionhuang/music/ui/viewholders/YouTubeViewHolder.kt @@ -36,11 +36,13 @@ class YouTubeHeaderViewHolder( ) : YouTubeViewHolder(viewGroup, R.layout.item_youtube_header) { fun bind(header: Header) { binding.header = header - header.moreNavigationEndpoint?.let { endpoint -> - binding.root.isClickable = true + binding.root.isClickable = header.moreNavigationEndpoint != null + if (header.moreNavigationEndpoint != null) { binding.root.setOnClickListener { - navigationEndpointHandler.handle(endpoint) + navigationEndpointHandler.handle(header.moreNavigationEndpoint) } + } else { + binding.root.setOnClickListener(null) } } } From a97760edb522d048f04c7e014f298d32555ff783 Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 1 Nov 2022 20:55:00 +0800 Subject: [PATCH 92/95] Fix #386 --- .../main/java/com/zionhuang/music/repos/YouTubeRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/zionhuang/music/repos/YouTubeRepository.kt b/app/src/main/java/com/zionhuang/music/repos/YouTubeRepository.kt index 60570cbaa..d1a961e04 100644 --- a/app/src/main/java/com/zionhuang/music/repos/YouTubeRepository.kt +++ b/app/src/main/java/com/zionhuang/music/repos/YouTubeRepository.kt @@ -63,7 +63,7 @@ class YouTubeRepository(val context: Context) { ) ) + browseResult.items ) - } else if (endpoint.isArtistEndpoint) { + } else if (endpoint.isArtistEndpoint && endpoint.params == null) { // inject library artist songs preview browseResult.copy( items = browseResult.items.toMutableList().apply { From 96a6e7a0276e195e5c9be0916663ecccd2e60a4e Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Tue, 1 Nov 2022 21:03:08 +0800 Subject: [PATCH 93/95] Update changelog --- fastlane/metadata/android/en-US/changelogs/14.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/fastlane/metadata/android/en-US/changelogs/14.txt b/fastlane/metadata/android/en-US/changelogs/14.txt index 17a24b807..0c9287f0a 100644 --- a/fastlane/metadata/android/en-US/changelogs/14.txt +++ b/fastlane/metadata/android/en-US/changelogs/14.txt @@ -24,6 +24,7 @@ - Fix ActivityNotFoundException in voice search #362 - Fix hardware bitmap exception #360 - Fix artists with no song may not be deleted automatically #381 +- Fix "From your library" displaying in artist album page #386 Translation From b94e68ecc4467e5b0c76938c81fe35b1676b6d1d Mon Sep 17 00:00:00 2001 From: Michael Moroni Date: Tue, 1 Nov 2022 16:19:37 +0100 Subject: [PATCH 94/95] Updated Italian changelog --- fastlane/metadata/android/it/changelogs/14.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/fastlane/metadata/android/it/changelogs/14.txt b/fastlane/metadata/android/it/changelogs/14.txt index 9914e72bd..46cdc6faf 100644 --- a/fastlane/metadata/android/it/changelogs/14.txt +++ b/fastlane/metadata/android/it/changelogs/14.txt @@ -24,6 +24,7 @@ - Sistemato ActivityNotFoundException nella ricerca vocale #362 - Sistemato eccezione di hardware bitmap #360 - Sistemata l'eliminazione automatica degli artisti privi di brani #381 +- Sistemato "Dalla tua libreria" che viene mostrata nella pagina degli album di un artista #386 Aggiornamenti della traduzione From af06c18a16dff6363f8a7e4f5539aaa4c93d665b Mon Sep 17 00:00:00 2001 From: Zion Huang Date: Wed, 2 Nov 2022 14:15:42 +0800 Subject: [PATCH 95/95] Update app description --- .../android/en-US/full_description.txt | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 4d14c16a9..326a658fe 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -7,21 +7,13 @@ The project is currently in an unstable stage. If you encounter bugs, please rep
Features: * Play songs without ads -* Browse almost any YouTube Music page -* Search songs, albums, videos and playlists from YouTube Music -* Open YouTube Music links -* Save songs, albums and playlists in local database * Download music for offline playback -* Edit song title -* Add links to your favorite YouTube Music playlists -* Material design player -* Lockscreen playback -* Media controls in notification -* Skip to next/previous song -* Repeat/shuffle mode -* Edit now-playing queue -* Custom themes -* Dark theme -* Localization -* Proxy -* Backup & restore \ No newline at end of file +* Local library management +* Export downloaded songs via SAF +* Cache songs +* (Synchronized) lyrics +* Audio normalization +* Skip silence +* Backup & restore +* Support proxy +* Support Android Auto \ No newline at end of file