Code refactor
Added new lyrics view and code refactor
This commit is contained in:
parent
e5407592c5
commit
8a2b803286
22 changed files with 1151 additions and 1065 deletions
|
@ -21,7 +21,7 @@ object Constants {
|
||||||
|
|
||||||
const val RATE_ON_GOOGLE_PLAY =
|
const val RATE_ON_GOOGLE_PLAY =
|
||||||
"https://play.google.com/store/apps/details?id=code.name.monkey.retromusic"
|
"https://play.google.com/store/apps/details?id=code.name.monkey.retromusic"
|
||||||
const val TRANSLATE = "http://monkeycodeapp.oneskyapp.com/collaboration/project?id=238534"
|
const val TRANSLATE = "https://github.com/h4h13/RetroMusicPlayer"
|
||||||
const val GITHUB_PROJECT = "https://github.com/h4h13/RetroMusicPlayer"
|
const val GITHUB_PROJECT = "https://github.com/h4h13/RetroMusicPlayer"
|
||||||
const val TELEGRAM_CHANGE_LOG = "https://t.me/retromusiclog"
|
const val TELEGRAM_CHANGE_LOG = "https://t.me/retromusiclog"
|
||||||
const val USER_PROFILE = "profile.jpg"
|
const val USER_PROFILE = "profile.jpg"
|
||||||
|
|
|
@ -1,72 +1,27 @@
|
||||||
package code.name.monkey.retromusic.activities
|
package code.name.monkey.retromusic.activities
|
||||||
|
|
||||||
import android.R.attr
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.res.ColorStateList
|
|
||||||
import android.os.AsyncTask
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.view.Menu
|
||||||
import android.view.*
|
import android.view.MenuItem
|
||||||
import androidx.annotation.StringRes
|
import android.view.WindowManager
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.fragment.app.FragmentStatePagerAdapter
|
|
||||||
import androidx.viewpager.widget.ViewPager
|
|
||||||
import code.name.monkey.appthemehelper.ThemeStore
|
import code.name.monkey.appthemehelper.ThemeStore
|
||||||
import code.name.monkey.appthemehelper.util.ATHUtil.resolveColor
|
|
||||||
import code.name.monkey.appthemehelper.util.ColorUtil
|
|
||||||
import code.name.monkey.appthemehelper.util.MaterialValueHelper
|
|
||||||
import code.name.monkey.appthemehelper.util.TintHelper
|
|
||||||
import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper
|
import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper
|
||||||
import code.name.monkey.retromusic.App
|
|
||||||
import code.name.monkey.retromusic.R
|
import code.name.monkey.retromusic.R
|
||||||
import code.name.monkey.retromusic.activities.base.AbsMusicServiceActivity
|
import code.name.monkey.retromusic.activities.base.AbsMusicServiceActivity
|
||||||
import code.name.monkey.retromusic.extensions.surfaceColor
|
import code.name.monkey.retromusic.extensions.surfaceColor
|
||||||
import code.name.monkey.retromusic.extensions.textColorSecondary
|
|
||||||
import code.name.monkey.retromusic.fragments.base.AbsMusicServiceFragment
|
|
||||||
import code.name.monkey.retromusic.helper.MusicPlayerRemote
|
import code.name.monkey.retromusic.helper.MusicPlayerRemote
|
||||||
import code.name.monkey.retromusic.helper.MusicProgressViewUpdateHelper
|
import code.name.monkey.retromusic.helper.MusicProgressViewUpdateHelper
|
||||||
import code.name.monkey.retromusic.lyrics.LrcHelper
|
import code.name.monkey.retromusic.lyrics.LrcView
|
||||||
import code.name.monkey.retromusic.model.Song
|
import code.name.monkey.retromusic.model.Song
|
||||||
import code.name.monkey.retromusic.model.lyrics.Lyrics
|
|
||||||
import code.name.monkey.retromusic.util.LyricUtil
|
import code.name.monkey.retromusic.util.LyricUtil
|
||||||
import code.name.monkey.retromusic.util.MusicUtil
|
import code.name.monkey.retromusic.util.RetroUtil
|
||||||
import code.name.monkey.retromusic.util.PreferenceUtil
|
|
||||||
import kotlinx.android.synthetic.main.activity_lyrics.*
|
import kotlinx.android.synthetic.main.activity_lyrics.*
|
||||||
import kotlinx.android.synthetic.main.fragment_lyrics.*
|
|
||||||
import kotlinx.android.synthetic.main.fragment_synced.*
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class LyricsActivity : AbsMusicServiceActivity(), View.OnClickListener,
|
class LyricsActivity : AbsMusicServiceActivity(), MusicProgressViewUpdateHelper.Callback {
|
||||||
ViewPager.OnPageChangeListener {
|
private lateinit var updateHelper: MusicProgressViewUpdateHelper
|
||||||
override fun onPageScrollStateChanged(state: Int) {
|
|
||||||
when (state) {
|
|
||||||
ViewPager.SCROLL_STATE_IDLE -> fab.show()
|
|
||||||
ViewPager.SCROLL_STATE_DRAGGING,
|
|
||||||
ViewPager.SCROLL_STATE_SETTLING -> fab.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageSelected(position: Int) {
|
|
||||||
PreferenceUtil.lyricsOption = position
|
|
||||||
if (position == 0) fab.text = getString(R.string.synced_lyrics)
|
|
||||||
else if (position == 1) fab.text = getString(R.string.lyrics)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(v: View?) {
|
|
||||||
when (viewPager.currentItem) {
|
|
||||||
0 -> showSyncedLyrics()
|
|
||||||
1 -> showLyricsSaveDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var song: Song
|
private lateinit var song: Song
|
||||||
private var lyricsString: String? = null
|
|
||||||
|
|
||||||
private val googleSearchLrcUrl: String
|
private val googleSearchLrcUrl: String
|
||||||
get() {
|
get() {
|
||||||
|
@ -84,56 +39,63 @@ class LyricsActivity : AbsMusicServiceActivity(), View.OnClickListener,
|
||||||
setTaskDescriptionColorAuto()
|
setTaskDescriptionColorAuto()
|
||||||
setNavigationbarColorAuto()
|
setNavigationbarColorAuto()
|
||||||
|
|
||||||
fab.backgroundTintList = ColorStateList.valueOf(ThemeStore.accentColor(this))
|
|
||||||
ColorStateList.valueOf(
|
|
||||||
MaterialValueHelper.getPrimaryTextColor(
|
|
||||||
this,
|
|
||||||
ColorUtil.isColorLight(ThemeStore.accentColor(this))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.apply {
|
|
||||||
fab.setTextColor(this)
|
|
||||||
fab.iconTint = this
|
|
||||||
}
|
|
||||||
setupWakelock()
|
setupWakelock()
|
||||||
|
|
||||||
viewPager.apply {
|
|
||||||
adapter = PagerAdapter(supportFragmentManager)
|
|
||||||
currentItem = PreferenceUtil.lyricsOption
|
|
||||||
addOnPageChangeListener(this@LyricsActivity)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
toolbar.setBackgroundColor(surfaceColor())
|
toolbar.setBackgroundColor(surfaceColor())
|
||||||
tabs.setBackgroundColor(surfaceColor())
|
|
||||||
ToolbarContentTintHelper.colorBackButton(toolbar)
|
ToolbarContentTintHelper.colorBackButton(toolbar)
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
tabs.setupWithViewPager(viewPager)
|
|
||||||
tabs.setSelectedTabIndicator(
|
|
||||||
TintHelper.createTintedDrawable(
|
|
||||||
ContextCompat.getDrawable(
|
|
||||||
this,
|
|
||||||
R.drawable.tab_indicator
|
|
||||||
), ThemeStore.accentColor(this)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
tabs.setTabTextColors(
|
|
||||||
textColorSecondary(),
|
|
||||||
ThemeStore.accentColor(this)
|
|
||||||
)
|
|
||||||
tabs.setSelectedTabIndicatorColor(ThemeStore.accentColor(this))
|
|
||||||
|
|
||||||
fab.setOnClickListener(this)
|
updateHelper = MusicProgressViewUpdateHelper(this, 500, 1000)
|
||||||
|
setupLyricsView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupLyricsView() {
|
||||||
|
lyricsView.apply {
|
||||||
|
setCurrentColor(ThemeStore.accentColor(context))
|
||||||
|
setTimeTextColor(ThemeStore.accentColor(context))
|
||||||
|
setTimelineColor(ThemeStore.accentColor(context))
|
||||||
|
setTimelineTextColor(ThemeStore.accentColor(context))
|
||||||
|
setDraggable(true, LrcView.OnPlayClickListener {
|
||||||
|
MusicPlayerRemote.seekTo(it.toInt())
|
||||||
|
return@OnPlayClickListener true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
updateHelper.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
updateHelper.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUpdateProgressViews(progress: Int, total: Int) {
|
||||||
|
lyricsView.updateTime(progress.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadLRCLyrics() {
|
||||||
|
lyricsView.setLabel("Empty")
|
||||||
|
val song = MusicPlayerRemote.currentSong
|
||||||
|
if (LyricUtil.isLrcOriginalFileExist(song.data)) {
|
||||||
|
lyricsView.loadLrc(LyricUtil.getLocalLyricOriginalFile(song.data))
|
||||||
|
} else if (LyricUtil.isLrcFileExist(song.title, song.artistName)) {
|
||||||
|
lyricsView.loadLrc(LyricUtil.getLocalLyricFile(song.title, song.artistName))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayingMetaChanged() {
|
override fun onPlayingMetaChanged() {
|
||||||
super.onPlayingMetaChanged()
|
super.onPlayingMetaChanged()
|
||||||
updateTitleSong()
|
updateTitleSong()
|
||||||
|
loadLRCLyrics()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceConnected() {
|
override fun onServiceConnected() {
|
||||||
super.onServiceConnected()
|
super.onServiceConnected()
|
||||||
updateTitleSong()
|
updateTitleSong()
|
||||||
|
loadLRCLyrics()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateTitleSong() {
|
private fun updateTitleSong() {
|
||||||
|
@ -146,269 +108,19 @@ class LyricsActivity : AbsMusicServiceActivity(), View.OnClickListener,
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
menuInflater.inflate(R.menu.menu_search, menu)
|
||||||
|
return super.onCreateOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
if (item.itemId == android.R.id.home) {
|
if (item.itemId == android.R.id.home) {
|
||||||
finish()
|
finish()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if (item.itemId == R.id.action_search) {
|
||||||
|
RetroUtil.openUrl(this, googleSearchLrcUrl)
|
||||||
|
}
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private fun showSyncedLyrics() {
|
|
||||||
var content = ""
|
|
||||||
try {
|
|
||||||
content = LyricUtil.getStringFromFile(song.title, song.artistName)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
/*val materialDialog = MaterialDialog(this)
|
|
||||||
.show {
|
|
||||||
title(R.string.add_time_framed_lryics)
|
|
||||||
negativeButton(R.string.action_search) {
|
|
||||||
RetroUtil.openUrl(this@LyricsActivity, googleSearchLrcUrl)
|
|
||||||
}
|
|
||||||
input(
|
|
||||||
hint = getString(R.string.paste_lyrics_here),
|
|
||||||
prefill = content,
|
|
||||||
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
|
||||||
) { _, input ->
|
|
||||||
LyricUtil.writeLrcToLoc(song.title, song.artistName, input.toString())
|
|
||||||
}
|
|
||||||
positiveButton(android.R.string.ok) {
|
|
||||||
updateSong()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialUtil.setTint(materialDialog.getInputLayout(), false)*/
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateSong() {
|
|
||||||
val page =
|
|
||||||
supportFragmentManager.findFragmentByTag("android:switcher:" + R.id.viewPager + ":" + viewPager.currentItem)
|
|
||||||
if (viewPager.currentItem == 0 && page != null) {
|
|
||||||
(page as BaseLyricsFragment).upDateSong()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLyricsSaveDialog() {
|
|
||||||
val content: String = if (lyricsString == null) {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
lyricsString!!
|
|
||||||
}
|
|
||||||
|
|
||||||
/*val materialDialog = MaterialDialog(
|
|
||||||
this
|
|
||||||
).show {
|
|
||||||
|
|
||||||
title(R.string.add_lyrics)
|
|
||||||
negativeButton(R.string.action_search) {
|
|
||||||
RetroUtil.openUrl(this@LyricsActivity, getGoogleSearchUrl())
|
|
||||||
}
|
|
||||||
input(
|
|
||||||
hint = getString(R.string.paste_lyrics_here),
|
|
||||||
prefill = content,
|
|
||||||
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
|
||||||
) { _, input ->
|
|
||||||
val fieldKeyValueMap = EnumMap<FieldKey, String>(FieldKey::class.java)
|
|
||||||
fieldKeyValueMap[FieldKey.LYRICS] = input.toString()
|
|
||||||
WriteTagsAsyncTask(this@LyricsActivity).execute(
|
|
||||||
WriteTagsAsyncTask.LoadingInfo(
|
|
||||||
getSongPaths(song), fieldKeyValueMap, null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
positiveButton(android.R.string.ok) {
|
|
||||||
updateSong()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MaterialUtil.setTint(materialDialog.getInputLayout(), false)*/
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSongPaths(song: Song): ArrayList<String> {
|
|
||||||
val paths = ArrayList<String>(1)
|
|
||||||
paths.add(song.data)
|
|
||||||
return paths
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getGoogleSearchUrl(): String {
|
|
||||||
var baseUrl = "http://www.google.com/search?"
|
|
||||||
var query = song.title + "+" + song.artistName
|
|
||||||
query = "q=" + query.replace(" ", "+") + " lyrics"
|
|
||||||
baseUrl += query
|
|
||||||
return baseUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
class PagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(
|
|
||||||
fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
|
|
||||||
) {
|
|
||||||
|
|
||||||
class Tabs(
|
|
||||||
@StringRes val title: Int, val fragment: Fragment
|
|
||||||
)
|
|
||||||
|
|
||||||
private var tabs = ArrayList<Tabs>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
|
||||||
tabs.add(Tabs(R.string.synced_lyrics, SyncedLyricsFragment()))
|
|
||||||
}
|
|
||||||
tabs.add(Tabs(R.string.normal_lyrics, OfflineLyricsFragment()))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItem(position: Int): Fragment {
|
|
||||||
return tabs[position].fragment
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPageTitle(position: Int): CharSequence? {
|
|
||||||
return App.getContext().getString(tabs[position].title)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCount(): Int {
|
|
||||||
return tabs.size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class BaseLyricsFragment : AbsMusicServiceFragment() {
|
|
||||||
abstract fun upDateSong()
|
|
||||||
|
|
||||||
override fun onPlayingMetaChanged() {
|
|
||||||
super.onPlayingMetaChanged()
|
|
||||||
upDateSong()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceConnected() {
|
|
||||||
super.onServiceConnected()
|
|
||||||
upDateSong()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OfflineLyricsFragment : BaseLyricsFragment() {
|
|
||||||
override fun upDateSong() {
|
|
||||||
loadSongLyrics()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var updateLyricsAsyncTask: AsyncTask<*, *, *>? = null
|
|
||||||
private var lyrics: Lyrics? = null
|
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
|
||||||
private fun loadSongLyrics() {
|
|
||||||
if (updateLyricsAsyncTask != null) {
|
|
||||||
updateLyricsAsyncTask!!.cancel(false)
|
|
||||||
}
|
|
||||||
val song = MusicPlayerRemote.currentSong
|
|
||||||
updateLyricsAsyncTask = object : AsyncTask<Void?, Void?, Lyrics?>() {
|
|
||||||
override fun doInBackground(vararg params: Void?): Lyrics? {
|
|
||||||
val data = MusicUtil.getLyrics(song)
|
|
||||||
return if (TextUtils.isEmpty(data)) {
|
|
||||||
null
|
|
||||||
} else Lyrics.parse(song, data!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPreExecute() {
|
|
||||||
super.onPreExecute()
|
|
||||||
lyrics = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPostExecute(l: Lyrics?) {
|
|
||||||
lyrics = l
|
|
||||||
offlineLyrics?.visibility = View.VISIBLE
|
|
||||||
if (l == null) {
|
|
||||||
offlineLyrics?.setText(R.string.no_lyrics_found)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
(activity as LyricsActivity).lyricsString = l.text
|
|
||||||
offlineLyrics?.text = l.text
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCancelled(s: Lyrics?) {
|
|
||||||
onPostExecute(null)
|
|
||||||
}
|
|
||||||
}.execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
|
||||||
super.onActivityCreated(savedInstanceState)
|
|
||||||
loadSongLyrics()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
if (updateLyricsAsyncTask != null && !updateLyricsAsyncTask!!.isCancelled) {
|
|
||||||
updateLyricsAsyncTask?.cancel(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
return inflater.inflate(R.layout.fragment_lyrics, container, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SyncedLyricsFragment : BaseLyricsFragment(), MusicProgressViewUpdateHelper.Callback {
|
|
||||||
override fun upDateSong() {
|
|
||||||
loadLRCLyrics()
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var updateHelper: MusicProgressViewUpdateHelper
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
return inflater.inflate(R.layout.fragment_synced, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
updateHelper = MusicProgressViewUpdateHelper(this, 500, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
setupLyricsView()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupLyricsView() {
|
|
||||||
lyricsView.apply {
|
|
||||||
setCurrentPlayLineColor(ThemeStore.accentColor(requireContext()))
|
|
||||||
setIndicatorTextColor(ThemeStore.accentColor(requireContext()))
|
|
||||||
setCurrentIndicateLineTextColor(
|
|
||||||
resolveColor(
|
|
||||||
requireContext(),
|
|
||||||
attr.textColorPrimary
|
|
||||||
)
|
|
||||||
)
|
|
||||||
setNoLrcTextColor(resolveColor(requireContext(), attr.textColorPrimary))
|
|
||||||
setOnPlayIndicatorLineListener { time, _ -> MusicPlayerRemote.seekTo(time.toInt()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
updateHelper.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
updateHelper.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUpdateProgressViews(progress: Int, total: Int) {
|
|
||||||
lyricsView.updateTime(progress.toLong())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadLRCLyrics() {
|
|
||||||
lyricsView.resetView("Empty")
|
|
||||||
val song = MusicPlayerRemote.currentSong
|
|
||||||
if (LyricUtil.isLrcFileExist(song.title, song.artistName)) {
|
|
||||||
showLyricsLocal(LyricUtil.getLocalLyricFile(song.title, song.artistName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLyricsLocal(file: File?) {
|
|
||||||
if (file != null) {
|
|
||||||
lyricsView.setLrcData(LrcHelper.parseLrcFromFile(file))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -201,7 +201,7 @@ class SkuDetailsAdapter(
|
||||||
|
|
||||||
override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
|
override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
|
||||||
val skuDetails = skuDetailsList[i]
|
val skuDetails = skuDetailsList[i]
|
||||||
viewHolder.title.text = skuDetails.title.replace("(Retro Music Player \uD83C\uDFB5)", "")
|
viewHolder.title.text = skuDetails.title.replace("Music Player - MP3 Player - Retro", "")
|
||||||
.trim { it <= ' ' }
|
.trim { it <= ' ' }
|
||||||
viewHolder.text.text = skuDetails.description
|
viewHolder.text.text = skuDetails.description
|
||||||
viewHolder.text.visibility = View.GONE
|
viewHolder.text.visibility = View.GONE
|
||||||
|
|
|
@ -63,7 +63,7 @@ class UserInfoActivity : AbsBaseActivity() {
|
||||||
next.setOnClickListener {
|
next.setOnClickListener {
|
||||||
val nameString = name.text.toString().trim { it <= ' ' }
|
val nameString = name.text.toString().trim { it <= ' ' }
|
||||||
if (TextUtils.isEmpty(nameString)) {
|
if (TextUtils.isEmpty(nameString)) {
|
||||||
Toast.makeText(this, "Umm name is empty", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "Umm you're name can't be empty!", Toast.LENGTH_SHORT).show()
|
||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
PreferenceUtil.userName = nameString
|
PreferenceUtil.userName = nameString
|
||||||
|
|
|
@ -178,5 +178,4 @@ private fun <E> ArrayList<E>.toPlaylist(): ArrayList<Playlist> {
|
||||||
arrayList.add(x as Playlist)
|
arrayList.add(x as Playlist)
|
||||||
}
|
}
|
||||||
return arrayList
|
return arrayList
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,16 @@ import code.name.monkey.retromusic.glide.RetroMusicColoredTarget
|
||||||
import code.name.monkey.retromusic.glide.SongGlideRequest
|
import code.name.monkey.retromusic.glide.SongGlideRequest
|
||||||
import code.name.monkey.retromusic.misc.CustomFragmentStatePagerAdapter
|
import code.name.monkey.retromusic.misc.CustomFragmentStatePagerAdapter
|
||||||
import code.name.monkey.retromusic.model.Song
|
import code.name.monkey.retromusic.model.Song
|
||||||
|
import code.name.monkey.retromusic.util.MusicUtil
|
||||||
import code.name.monkey.retromusic.util.NavigationUtil
|
import code.name.monkey.retromusic.util.NavigationUtil
|
||||||
|
|
||||||
import code.name.monkey.retromusic.util.PreferenceUtil
|
import code.name.monkey.retromusic.util.PreferenceUtil
|
||||||
import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
|
import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class AlbumCoverPagerAdapter(
|
class AlbumCoverPagerAdapter(
|
||||||
fragmentManager: FragmentManager,
|
fragmentManager: FragmentManager,
|
||||||
|
@ -85,20 +90,33 @@ class AlbumCoverPagerAdapter(
|
||||||
val view = inflater.inflate(getLayoutWithPlayerTheme(), container, false)
|
val view = inflater.inflate(getLayoutWithPlayerTheme(), container, false)
|
||||||
albumCover = view.findViewById(R.id.player_image)
|
albumCover = view.findViewById(R.id.player_image)
|
||||||
albumCover.setOnClickListener {
|
albumCover.setOnClickListener {
|
||||||
NavigationUtil.goToLyrics(requireActivity())
|
showLyricsDialog()
|
||||||
}
|
}
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showLyricsDialog() {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val data = MusicUtil.getLyrics(song)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
MaterialAlertDialogBuilder(
|
||||||
|
requireContext(),
|
||||||
|
R.style.ThemeOverlay_MaterialComponents_Dialog_Alert
|
||||||
|
).apply {
|
||||||
|
setTitle(song.title)
|
||||||
|
setMessage(data)
|
||||||
|
setNegativeButton(R.string.synced_lyrics) { _, _ ->
|
||||||
|
NavigationUtil.goToLyrics(requireActivity())
|
||||||
|
}
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getLayoutWithPlayerTheme(): Int {
|
private fun getLayoutWithPlayerTheme(): Int {
|
||||||
return when (PreferenceUtil.nowPlayingScreen) {
|
return when (PreferenceUtil.nowPlayingScreen) {
|
||||||
Card,
|
Card, Fit, Tiny, Classic, Peak, Gradient, Full -> R.layout.fragment_album_full_cover
|
||||||
Fit,
|
|
||||||
Tiny,
|
|
||||||
Classic,
|
|
||||||
Peak,
|
|
||||||
Gradient,
|
|
||||||
Full -> R.layout.fragment_album_full_cover
|
|
||||||
else -> {
|
else -> {
|
||||||
if (PreferenceUtil.isCarouselEffect) {
|
if (PreferenceUtil.isCarouselEffect) {
|
||||||
R.layout.fragment_album_carousel_cover
|
R.layout.fragment_album_carousel_cover
|
||||||
|
|
|
@ -33,7 +33,6 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(), ViewPager.OnPageChan
|
||||||
fun removeSlideEffect() {
|
fun removeSlideEffect() {
|
||||||
val transformer = ParallaxPagerTransformer(R.id.player_image)
|
val transformer = ParallaxPagerTransformer(R.id.player_image)
|
||||||
transformer.setSpeed(0.3f)
|
transformer.setSpeed(0.3f)
|
||||||
//viewPager.setPageTransformer(true, transformer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
|
|
|
@ -4,8 +4,6 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import code.name.monkey.appthemehelper.util.ATHUtil
|
import code.name.monkey.appthemehelper.util.ATHUtil
|
||||||
import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper
|
import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper
|
||||||
|
@ -16,104 +14,11 @@ import code.name.monkey.retromusic.extensions.textColorSecondary
|
||||||
import code.name.monkey.retromusic.fragments.base.AbsPlayerFragment
|
import code.name.monkey.retromusic.fragments.base.AbsPlayerFragment
|
||||||
import code.name.monkey.retromusic.fragments.player.PlayerAlbumCoverFragment
|
import code.name.monkey.retromusic.fragments.player.PlayerAlbumCoverFragment
|
||||||
import code.name.monkey.retromusic.helper.MusicPlayerRemote
|
import code.name.monkey.retromusic.helper.MusicPlayerRemote
|
||||||
import code.name.monkey.retromusic.helper.MusicProgressViewUpdateHelper
|
|
||||||
import code.name.monkey.retromusic.model.Song
|
import code.name.monkey.retromusic.model.Song
|
||||||
import code.name.monkey.retromusic.model.lyrics.AbsSynchronizedLyrics
|
|
||||||
import code.name.monkey.retromusic.model.lyrics.Lyrics
|
|
||||||
import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
|
import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
|
||||||
import kotlinx.android.synthetic.main.fragment_adaptive_player.*
|
import kotlinx.android.synthetic.main.fragment_adaptive_player.*
|
||||||
|
|
||||||
class AdaptiveFragment : AbsPlayerFragment(), MusicProgressViewUpdateHelper.Callback {
|
class AdaptiveFragment : AbsPlayerFragment() {
|
||||||
|
|
||||||
private lateinit var lyricsLayout: FrameLayout
|
|
||||||
private lateinit var lyricsLine1: TextView
|
|
||||||
private lateinit var lyricsLine2: TextView
|
|
||||||
|
|
||||||
private var lyrics: Lyrics? = null
|
|
||||||
private lateinit var progressViewUpdateHelper: MusicProgressViewUpdateHelper
|
|
||||||
|
|
||||||
override fun onUpdateProgressViews(progress: Int, total: Int) {
|
|
||||||
if (!isLyricsLayoutBound()) return
|
|
||||||
|
|
||||||
if (!isLyricsLayoutVisible()) {
|
|
||||||
hideLyricsLayout()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lyrics !is AbsSynchronizedLyrics) return
|
|
||||||
val synchronizedLyrics = lyrics as AbsSynchronizedLyrics
|
|
||||||
|
|
||||||
lyricsLayout.visibility = View.VISIBLE
|
|
||||||
lyricsLayout.alpha = 1f
|
|
||||||
|
|
||||||
val oldLine = lyricsLine2.text.toString()
|
|
||||||
val line = synchronizedLyrics.getLine(progress)
|
|
||||||
|
|
||||||
if (oldLine != line || oldLine.isEmpty()) {
|
|
||||||
lyricsLine1.text = oldLine
|
|
||||||
lyricsLine2.text = line
|
|
||||||
|
|
||||||
lyricsLine1.visibility = View.VISIBLE
|
|
||||||
lyricsLine2.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
lyricsLine2.measure(
|
|
||||||
View.MeasureSpec.makeMeasureSpec(
|
|
||||||
lyricsLine2.measuredWidth,
|
|
||||||
View.MeasureSpec.EXACTLY
|
|
||||||
),
|
|
||||||
View.MeasureSpec.UNSPECIFIED
|
|
||||||
)
|
|
||||||
val h: Float = lyricsLine2.measuredHeight.toFloat()
|
|
||||||
|
|
||||||
lyricsLine1.alpha = 1f
|
|
||||||
lyricsLine1.translationY = 0f
|
|
||||||
lyricsLine1.animate().alpha(0f).translationY(-h).duration = VISIBILITY_ANIM_DURATION
|
|
||||||
|
|
||||||
lyricsLine2.alpha = 0f
|
|
||||||
lyricsLine2.translationY = h
|
|
||||||
lyricsLine2.animate().alpha(1f).translationY(0f).duration = VISIBILITY_ANIM_DURATION
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isLyricsLayoutVisible(): Boolean {
|
|
||||||
return lyrics != null && lyrics!!.isSynchronized && lyrics!!.isValid
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isLyricsLayoutBound(): Boolean {
|
|
||||||
return lyricsLayout != null && lyricsLine1 != null && lyricsLine2 != null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hideLyricsLayout() {
|
|
||||||
lyricsLayout.animate().alpha(0f).setDuration(VISIBILITY_ANIM_DURATION)
|
|
||||||
.withEndAction(Runnable {
|
|
||||||
if (!isLyricsLayoutBound()) return@Runnable
|
|
||||||
lyricsLayout.visibility = View.GONE
|
|
||||||
lyricsLine1.text = null
|
|
||||||
lyricsLine2.text = null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setLyrics(l: Lyrics?) {
|
|
||||||
lyrics = l
|
|
||||||
|
|
||||||
if (!isLyricsLayoutBound()) return
|
|
||||||
|
|
||||||
if (!isLyricsLayoutVisible()) {
|
|
||||||
hideLyricsLayout()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lyricsLine1.text = null
|
|
||||||
lyricsLine2.text = null
|
|
||||||
|
|
||||||
lyricsLayout.visibility = View.VISIBLE
|
|
||||||
lyricsLayout.animate().alpha(1f).duration = VISIBILITY_ANIM_DURATION
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
progressViewUpdateHelper.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun playerToolbar(): Toolbar {
|
override fun playerToolbar(): Toolbar {
|
||||||
return playerToolbar
|
return playerToolbar
|
||||||
|
@ -132,15 +37,8 @@ class AdaptiveFragment : AbsPlayerFragment(), MusicProgressViewUpdateHelper.Call
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
lyricsLayout = view.findViewById(R.id.player_lyrics)
|
|
||||||
lyricsLine1 = view.findViewById(R.id.player_lyrics_line1)
|
|
||||||
lyricsLine2 = view.findViewById(R.id.player_lyrics_line2)
|
|
||||||
|
|
||||||
setUpSubFragments()
|
setUpSubFragments()
|
||||||
setUpPlayerToolbar()
|
setUpPlayerToolbar()
|
||||||
|
|
||||||
progressViewUpdateHelper = MusicProgressViewUpdateHelper(this, 500, 1000)
|
|
||||||
progressViewUpdateHelper.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setUpSubFragments() {
|
private fun setUpSubFragments() {
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 wangchenyan
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
|
||||||
|
* except in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the
|
||||||
|
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the specific language governing
|
||||||
|
* permissions and limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package code.name.monkey.retromusic.lyrics;
|
||||||
|
|
||||||
|
import android.text.Layout;
|
||||||
|
import android.text.StaticLayout;
|
||||||
|
import android.text.TextPaint;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一行歌词实体
|
||||||
|
*/
|
||||||
|
class LrcEntry implements Comparable<LrcEntry> {
|
||||||
|
private long time;
|
||||||
|
private String text;
|
||||||
|
private String secondText;
|
||||||
|
private StaticLayout staticLayout;
|
||||||
|
/**
|
||||||
|
* 歌词距离视图顶部的距离
|
||||||
|
*/
|
||||||
|
private float offset = Float.MIN_VALUE;
|
||||||
|
public static final int GRAVITY_CENTER = 0;
|
||||||
|
public static final int GRAVITY_LEFT = 1;
|
||||||
|
public static final int GRAVITY_RIGHT = 2;
|
||||||
|
|
||||||
|
LrcEntry(long time, String text) {
|
||||||
|
this.time = time;
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
LrcEntry(long time, String text, String secondText) {
|
||||||
|
this.time = time;
|
||||||
|
this.text = text;
|
||||||
|
this.secondText = secondText;
|
||||||
|
}
|
||||||
|
|
||||||
|
void init(TextPaint paint, int width, int gravity) {
|
||||||
|
Layout.Alignment align;
|
||||||
|
switch (gravity) {
|
||||||
|
case GRAVITY_LEFT:
|
||||||
|
align = Layout.Alignment.ALIGN_NORMAL;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
case GRAVITY_CENTER:
|
||||||
|
align = Layout.Alignment.ALIGN_CENTER;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GRAVITY_RIGHT:
|
||||||
|
align = Layout.Alignment.ALIGN_OPPOSITE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
staticLayout = new StaticLayout(getShowText(), paint, width, align, 1f, 0f, false);
|
||||||
|
|
||||||
|
offset = Float.MIN_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
long getTime() {
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
|
StaticLayout getStaticLayout() {
|
||||||
|
return staticLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getHeight() {
|
||||||
|
if (staticLayout == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return staticLayout.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getOffset() {
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOffset(float offset) {
|
||||||
|
this.offset = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getText() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void setSecondText(String secondText) {
|
||||||
|
this.secondText = secondText;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getShowText() {
|
||||||
|
if (!TextUtils.isEmpty(secondText)) {
|
||||||
|
return text + "\n" + secondText;
|
||||||
|
} else {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(LrcEntry entry) {
|
||||||
|
if (entry == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return (int) (time - entry.getTime());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,232 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 wangchenyan
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
|
||||||
|
* except in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the
|
||||||
|
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the specific language governing
|
||||||
|
* permissions and limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package code.name.monkey.retromusic.lyrics;
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.format.DateUtils;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具类
|
||||||
|
*/
|
||||||
|
class LrcUtils {
|
||||||
|
private static final Pattern PATTERN_LINE = Pattern.compile("((\\[\\d\\d:\\d\\d\\.\\d{2,3}\\])+)(.+)");
|
||||||
|
private static final Pattern PATTERN_TIME = Pattern.compile("\\[(\\d\\d):(\\d\\d)\\.(\\d{2,3})\\]");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从文件解析双语歌词
|
||||||
|
*/
|
||||||
|
static List<LrcEntry> parseLrc(File[] lrcFiles) {
|
||||||
|
if (lrcFiles == null || lrcFiles.length != 2 || lrcFiles[0] == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
File mainLrcFile = lrcFiles[0];
|
||||||
|
File secondLrcFile = lrcFiles[1];
|
||||||
|
List<LrcEntry> mainEntryList = parseLrc(mainLrcFile);
|
||||||
|
List<LrcEntry> secondEntryList = parseLrc(secondLrcFile);
|
||||||
|
|
||||||
|
if (mainEntryList != null && secondEntryList != null) {
|
||||||
|
for (LrcEntry mainEntry : mainEntryList) {
|
||||||
|
for (LrcEntry secondEntry : secondEntryList) {
|
||||||
|
if (mainEntry.getTime() == secondEntry.getTime()) {
|
||||||
|
mainEntry.setSecondText(secondEntry.getText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mainEntryList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从文件解析歌词
|
||||||
|
*/
|
||||||
|
private static List<LrcEntry> parseLrc(File lrcFile) {
|
||||||
|
if (lrcFile == null || !lrcFile.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LrcEntry> entryList = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(lrcFile), "utf-8"));
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
List<LrcEntry> list = parseLine(line);
|
||||||
|
if (list != null && !list.isEmpty()) {
|
||||||
|
entryList.addAll(list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
br.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
Collections.sort(entryList);
|
||||||
|
return entryList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从文本解析双语歌词
|
||||||
|
*/
|
||||||
|
static List<LrcEntry> parseLrc(String[] lrcTexts) {
|
||||||
|
if (lrcTexts == null || lrcTexts.length != 2 || TextUtils.isEmpty(lrcTexts[0])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String mainLrcText = lrcTexts[0];
|
||||||
|
String secondLrcText = lrcTexts[1];
|
||||||
|
List<LrcEntry> mainEntryList = parseLrc(mainLrcText);
|
||||||
|
List<LrcEntry> secondEntryList = parseLrc(secondLrcText);
|
||||||
|
|
||||||
|
if (mainEntryList != null && secondEntryList != null) {
|
||||||
|
for (LrcEntry mainEntry : mainEntryList) {
|
||||||
|
for (LrcEntry secondEntry : secondEntryList) {
|
||||||
|
if (mainEntry.getTime() == secondEntry.getTime()) {
|
||||||
|
mainEntry.setSecondText(secondEntry.getText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mainEntryList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从文本解析歌词
|
||||||
|
*/
|
||||||
|
private static List<LrcEntry> parseLrc(String lrcText) {
|
||||||
|
if (TextUtils.isEmpty(lrcText)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lrcText.startsWith("\uFEFF")) {
|
||||||
|
lrcText = lrcText.replace("\uFEFF", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LrcEntry> entryList = new ArrayList<>();
|
||||||
|
String[] array = lrcText.split("\\n");
|
||||||
|
for (String line : array) {
|
||||||
|
List<LrcEntry> list = parseLine(line);
|
||||||
|
if (list != null && !list.isEmpty()) {
|
||||||
|
entryList.addAll(list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Collections.sort(entryList);
|
||||||
|
return entryList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取网络文本,需要在工作线程中执行
|
||||||
|
*/
|
||||||
|
static String getContentFromNetwork(String url, String charset) {
|
||||||
|
String lrcText = null;
|
||||||
|
try {
|
||||||
|
URL _url = new URL(url);
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) _url.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setConnectTimeout(10000);
|
||||||
|
conn.setReadTimeout(10000);
|
||||||
|
if (conn.getResponseCode() == 200) {
|
||||||
|
InputStream is = conn.getInputStream();
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int len;
|
||||||
|
while ((len = is.read(buffer)) != -1) {
|
||||||
|
bos.write(buffer, 0, len);
|
||||||
|
}
|
||||||
|
is.close();
|
||||||
|
bos.close();
|
||||||
|
lrcText = bos.toString(charset);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return lrcText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析一行歌词
|
||||||
|
*/
|
||||||
|
private static List<LrcEntry> parseLine(String line) {
|
||||||
|
if (TextUtils.isEmpty(line)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
line = line.trim();
|
||||||
|
// [00:17.65]让我掉下眼泪的
|
||||||
|
Matcher lineMatcher = PATTERN_LINE.matcher(line);
|
||||||
|
if (!lineMatcher.matches()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String times = lineMatcher.group(1);
|
||||||
|
String text = lineMatcher.group(3);
|
||||||
|
List<LrcEntry> entryList = new ArrayList<>();
|
||||||
|
|
||||||
|
// [00:17.65]
|
||||||
|
Matcher timeMatcher = PATTERN_TIME.matcher(times);
|
||||||
|
while (timeMatcher.find()) {
|
||||||
|
long min = Long.parseLong(timeMatcher.group(1));
|
||||||
|
long sec = Long.parseLong(timeMatcher.group(2));
|
||||||
|
String milString = timeMatcher.group(3);
|
||||||
|
long mil = Long.parseLong(milString);
|
||||||
|
// 如果毫秒是两位数,需要乘以10
|
||||||
|
if (milString.length() == 2) {
|
||||||
|
mil = mil * 10;
|
||||||
|
}
|
||||||
|
long time = min * DateUtils.MINUTE_IN_MILLIS + sec * DateUtils.SECOND_IN_MILLIS + mil;
|
||||||
|
entryList.add(new LrcEntry(time, text));
|
||||||
|
}
|
||||||
|
return entryList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转为[分:秒]
|
||||||
|
*/
|
||||||
|
static String formatTime(long milli) {
|
||||||
|
int m = (int) (milli / DateUtils.MINUTE_IN_MILLIS);
|
||||||
|
int s = (int) ((milli / DateUtils.SECOND_IN_MILLIS) % 60);
|
||||||
|
String mm = String.format(Locale.getDefault(), "%02d", m);
|
||||||
|
String ss = String.format(Locale.getDefault(), "%02d", s);
|
||||||
|
return mm + ":" + ss;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void resetDurationScale() {
|
||||||
|
try {
|
||||||
|
Field mField = ValueAnimator.class.getDeclaredField("sDurationScale");
|
||||||
|
mField.setAccessible(true);
|
||||||
|
mField.setFloat(null, 1);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -72,6 +72,11 @@ public class LyricUtil {
|
||||||
return file.exists();
|
return file.exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isLrcOriginalFileExist(@NonNull String path) {
|
||||||
|
File file = new File(getLrcOriginalPath(path));
|
||||||
|
return file.exists();
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static File getLocalLyricFile(@NonNull String title, @NonNull String artist) {
|
public static File getLocalLyricFile(@NonNull String title, @NonNull String artist) {
|
||||||
File file = new File(getLrcPath(title, artist));
|
File file = new File(getLrcPath(title, artist));
|
||||||
|
@ -82,10 +87,24 @@ public class LyricUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static File getLocalLyricOriginalFile(@NonNull String path) {
|
||||||
|
File file = new File(getLrcOriginalPath(path));
|
||||||
|
if (file.exists()) {
|
||||||
|
return file;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static String getLrcPath(String title, String artist) {
|
private static String getLrcPath(String title, String artist) {
|
||||||
return lrcRootPath + title + " - " + artist + ".lrc";
|
return lrcRootPath + title + " - " + artist + ".lrc";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String getLrcOriginalPath(String filePath) {
|
||||||
|
return filePath.replace(filePath.substring(filePath.lastIndexOf(".") + 1, filePath.length()), "lrc");
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static String decryptBASE64(@NonNull String str) {
|
public static String decryptBASE64(@NonNull String str) {
|
||||||
if (str == null || str.length() == 0) {
|
if (str == null || str.length() == 0) {
|
||||||
|
|
|
@ -29,8 +29,6 @@ import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import code.name.monkey.retromusic.R;
|
import code.name.monkey.retromusic.R;
|
||||||
import code.name.monkey.retromusic.activities.AboutActivity;
|
import code.name.monkey.retromusic.activities.AboutActivity;
|
||||||
import code.name.monkey.retromusic.activities.albums.AlbumDetailsActivity;
|
|
||||||
import code.name.monkey.retromusic.activities.artists.ArtistDetailActivity;
|
|
||||||
import code.name.monkey.retromusic.activities.DriveModeActivity;
|
import code.name.monkey.retromusic.activities.DriveModeActivity;
|
||||||
import code.name.monkey.retromusic.activities.GenreDetailsActivity;
|
import code.name.monkey.retromusic.activities.GenreDetailsActivity;
|
||||||
import code.name.monkey.retromusic.activities.LicenseActivity;
|
import code.name.monkey.retromusic.activities.LicenseActivity;
|
||||||
|
@ -43,6 +41,8 @@ import code.name.monkey.retromusic.activities.SettingsActivity;
|
||||||
import code.name.monkey.retromusic.activities.SupportDevelopmentActivity;
|
import code.name.monkey.retromusic.activities.SupportDevelopmentActivity;
|
||||||
import code.name.monkey.retromusic.activities.UserInfoActivity;
|
import code.name.monkey.retromusic.activities.UserInfoActivity;
|
||||||
import code.name.monkey.retromusic.activities.WhatsNewActivity;
|
import code.name.monkey.retromusic.activities.WhatsNewActivity;
|
||||||
|
import code.name.monkey.retromusic.activities.albums.AlbumDetailsActivity;
|
||||||
|
import code.name.monkey.retromusic.activities.artists.ArtistDetailActivity;
|
||||||
import code.name.monkey.retromusic.activities.bugreport.BugReportActivity;
|
import code.name.monkey.retromusic.activities.bugreport.BugReportActivity;
|
||||||
import code.name.monkey.retromusic.helper.MusicPlayerRemote;
|
import code.name.monkey.retromusic.helper.MusicPlayerRemote;
|
||||||
import code.name.monkey.retromusic.model.Genre;
|
import code.name.monkey.retromusic.model.Genre;
|
||||||
|
|
|
@ -96,7 +96,10 @@ object PreferenceUtil {
|
||||||
val languageCode get() = sharedPreferences.getString(LANGUAGE_NAME, "auto")
|
val languageCode get() = sharedPreferences.getString(LANGUAGE_NAME, "auto")
|
||||||
|
|
||||||
var userName
|
var userName
|
||||||
get() = sharedPreferences.getString(USER_NAME, "User Name")
|
get() = sharedPreferences.getString(
|
||||||
|
USER_NAME,
|
||||||
|
App.getContext().getString(R.string.user_name)
|
||||||
|
)
|
||||||
set(value) = sharedPreferences.edit {
|
set(value) = sharedPreferences.edit {
|
||||||
putString(USER_NAME, value)
|
putString(USER_NAME, value)
|
||||||
}
|
}
|
||||||
|
@ -420,7 +423,7 @@ object PreferenceUtil {
|
||||||
var songGridSize
|
var songGridSize
|
||||||
get() = sharedPreferences.getInt(
|
get() = sharedPreferences.getInt(
|
||||||
SONG_GRID_SIZE,
|
SONG_GRID_SIZE,
|
||||||
App.getContext().getIntRes(R.integer.default_grid_columns)
|
App.getContext().getIntRes(R.integer.default_list_columns)
|
||||||
)
|
)
|
||||||
set(value) = sharedPreferences.edit {
|
set(value) = sharedPreferences.edit {
|
||||||
putInt(SONG_GRID_SIZE, value)
|
putInt(SONG_GRID_SIZE, value)
|
||||||
|
|
|
@ -8,11 +8,8 @@
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/appBarLayout"
|
android:id="@+id/appBarLayout"
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@android:color/transparent"
|
android:layout_width="match_parent">
|
||||||
android:elevation="0dp"
|
|
||||||
app:elevation="0dp">
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
|
@ -22,7 +19,7 @@
|
||||||
app:contentInsetLeft="0dp"
|
app:contentInsetLeft="0dp"
|
||||||
app:contentInsetStart="0dp"
|
app:contentInsetStart="0dp"
|
||||||
app:contentInsetStartWithNavigation="0dp"
|
app:contentInsetStartWithNavigation="0dp"
|
||||||
app:navigationIcon="@drawable/ic_keyboard_arrow_down_black_24dp"
|
app:navigationIcon="@drawable/ic_keyboard_backspace_black_24dp"
|
||||||
app:subtitleTextAppearance="@style/TextViewCaption"
|
app:subtitleTextAppearance="@style/TextViewCaption"
|
||||||
app:titleMargin="0dp"
|
app:titleMargin="0dp"
|
||||||
app:titleMarginStart="0dp"
|
app:titleMarginStart="0dp"
|
||||||
|
@ -30,38 +27,15 @@
|
||||||
tools:subtitle="@tools:sample/full_names"
|
tools:subtitle="@tools:sample/full_names"
|
||||||
tools:title="@tools:sample/full_names" />
|
tools:title="@tools:sample/full_names" />
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabLayout
|
|
||||||
android:id="@+id/tabs"
|
|
||||||
style="@style/TabLayoutStyle"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
app:tabTextAppearance="@style/TabTextAppearance">
|
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabItem
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/synced_lyrics" />
|
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabItem
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/normal_lyrics" />
|
|
||||||
</com.google.android.material.tabs.TabLayout>
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
|
||||||
<androidx.viewpager.widget.ViewPager
|
<code.name.monkey.retromusic.lyrics.LrcView
|
||||||
android:id="@+id/viewPager"
|
android:id="@+id/lyricsView"
|
||||||
|
app:lrcLabel="@string/no_lyrics_found"
|
||||||
|
app:lrcPadding="16dp"
|
||||||
|
app:lrcTextGravity="left"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
|
||||||
android:id="@+id/fab"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|end"
|
|
||||||
android:layout_margin="16dp"
|
|
||||||
android:text="@string/edit"
|
|
||||||
app:icon="@drawable/ic_edit_white_24dp" />
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -84,6 +84,7 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
android:background="?attr/roundSelector"
|
android:background="?attr/roundSelector"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/volumeFragmentContainer"
|
app:layout_constraintBottom_toTopOf="@+id/volumeFragmentContainer"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/nextButton"
|
app:layout_constraintEnd_toStartOf="@+id/nextButton"
|
||||||
|
|
9
app/src/main/res/menu/menu_search.xml
Normal file
9
app/src/main/res/menu/menu_search.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_search"
|
||||||
|
android:icon="@drawable/ic_search_white_24dp"
|
||||||
|
android:title="@string/action_search"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
</menu>
|
|
@ -212,7 +212,7 @@
|
||||||
<item>sr</item>
|
<item>sr</item>
|
||||||
<item>sk</item>
|
<item>sk</item>
|
||||||
<item>es</item>
|
<item>es</item>
|
||||||
<item>sw</item>
|
<item>sv</item>
|
||||||
<item>ta</item>
|
<item>ta</item>
|
||||||
<item>te</item>
|
<item>te</item>
|
||||||
<item>tr</item>
|
<item>tr</item>
|
||||||
|
|
8
app/src/main/res/values/lrc_colors.xml
Normal file
8
app/src/main/res/values/lrc_colors.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="lrc_normal_text_color">#9E9E9E</color>
|
||||||
|
<color name="lrc_current_text_color">#FF4081</color>
|
||||||
|
<color name="lrc_timeline_text_color">#F8BBD0</color>
|
||||||
|
<color name="lrc_timeline_color">#809E9E9E</color>
|
||||||
|
<color name="lrc_time_text_color">#809E9E9E</color>
|
||||||
|
</resources>
|
10
app/src/main/res/values/lrc_dimens.xml
Normal file
10
app/src/main/res/values/lrc_dimens.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<integer name="lrc_animation_duration">1000</integer>
|
||||||
|
<dimen name="lrc_text_size">16sp</dimen>
|
||||||
|
<dimen name="lrc_time_text_size">12sp</dimen>
|
||||||
|
<dimen name="lrc_divider_height">16dp</dimen>
|
||||||
|
<dimen name="lrc_timeline_height">1dp</dimen>
|
||||||
|
<dimen name="lrc_drawable_width">30dp</dimen>
|
||||||
|
<dimen name="lrc_time_width">40dp</dimen>
|
||||||
|
</resources>
|
|
@ -14,25 +14,24 @@
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
<declare-styleable name="LrcView">
|
<declare-styleable name="LrcView">
|
||||||
<attr name="lrcTextSize" format="dimension"/>
|
<attr name="lrcTextSize" format="dimension" />
|
||||||
<attr name="lrcLineSpaceSize" format="dimension"/>
|
<attr name="lrcNormalTextSize" format="dimension" />
|
||||||
<attr name="lrcNormalTextColor" format="reference|color"/>
|
<attr name="lrcDividerHeight" format="dimension" />
|
||||||
<attr name="lrcCurrentTextColor" format="reference|color"/>
|
<attr name="lrcNormalTextColor" format="reference|color" />
|
||||||
<attr name="lrcTouchDelay" format="integer"/>
|
<attr name="lrcCurrentTextColor" format="reference|color" />
|
||||||
<attr name="noLrcTextSize" format="dimension"/>
|
<attr name="lrcTimelineTextColor" format="reference|color" />
|
||||||
<attr name="noLrcTextColor" format="reference|color"/>
|
<attr name="lrcAnimationDuration" format="integer" />
|
||||||
<attr name="indicatorLineHeight" format="dimension"/>
|
<attr name="lrcLabel" format="string" />
|
||||||
<attr name="indicatorTextSize" format="dimension"/>
|
<attr name="lrcPadding" format="dimension" />
|
||||||
<attr name="indicatorTextColor" format="reference|color"/>
|
<attr name="lrcTimelineColor" format="reference|color" />
|
||||||
<attr name="currentIndicateLrcColor" format="reference|color"/>
|
<attr name="lrcTimelineHeight" format="dimension" />
|
||||||
<attr name="indicatorTouchDelay" format="integer"/>
|
<attr name="lrcPlayDrawable" format="reference" />
|
||||||
<attr name="indicatorLineColor" format="reference|color"/>
|
<attr name="lrcTimeTextColor" format="reference|color" />
|
||||||
<attr name="indicatorStartEndMargin" format="dimension"/>
|
<attr name="lrcTimeTextSize" format="dimension" />
|
||||||
<attr name="iconLineGap" format="dimension"/>
|
<attr name="lrcTextGravity">
|
||||||
<attr name="playIconWidth" format="dimension"/>
|
<enum name="center" value="0" />
|
||||||
<attr name="playIconHeight" format="dimension"/>
|
<enum name="left" value="1" />
|
||||||
<attr name="playIcon" format="reference"/>
|
<enum name="right" value="2" />
|
||||||
<attr name="isLrcCurrentTextBold" format="boolean"/>
|
</attr>
|
||||||
<attr name="isLrcIndicatorTextBold" format="boolean"/>
|
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
</resources>
|
</resources>
|
|
@ -856,6 +856,7 @@
|
||||||
<string name="share_summary">Share the app with your friends and family</string>
|
<string name="share_summary">Share the app with your friends and family</string>
|
||||||
<string name="help_summary">Need more help?</string>
|
<string name="help_summary">Need more help?</string>
|
||||||
<string name="gradient">Gradient</string>
|
<string name="gradient">Gradient</string>
|
||||||
|
<string name="user_name">User Name</string>
|
||||||
|
|
||||||
<plurals name="albumSongs">
|
<plurals name="albumSongs">
|
||||||
<item quantity="one">Song</item>
|
<item quantity="one">Song</item>
|
||||||
|
|
Loading…
Reference in a new issue