|
|
@@ -0,0 +1,253 @@
|
|
|
+package com.adealink.weparty.music.local
|
|
|
+
|
|
|
+import android.animation.ObjectAnimator
|
|
|
+import android.animation.ValueAnimator
|
|
|
+import android.content.Intent
|
|
|
+import android.media.MediaScannerConnection
|
|
|
+import android.net.Uri
|
|
|
+import android.os.Bundle
|
|
|
+import android.view.View
|
|
|
+import android.view.animation.LinearInterpolator
|
|
|
+import androidx.activity.result.ActivityResult
|
|
|
+import androidx.activity.result.contract.ActivityResultContracts
|
|
|
+import androidx.annotation.OptIn
|
|
|
+import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
|
|
+import androidx.constraintlayout.widget.ConstraintLayout
|
|
|
+import androidx.core.view.updateLayoutParams
|
|
|
+import androidx.fragment.app.viewModels
|
|
|
+import androidx.media3.common.MimeTypes
|
|
|
+import androidx.media3.common.MimeTypes.BASE_TYPE_AUDIO
|
|
|
+import androidx.media3.common.util.UnstableApi
|
|
|
+import androidx.recyclerview.widget.LinearLayoutManager
|
|
|
+import androidx.recyclerview.widget.RecyclerView
|
|
|
+import com.adealink.frame.base.Rlt
|
|
|
+import com.adealink.frame.base.fastLazy
|
|
|
+import com.adealink.frame.ext.isViewBindingValid
|
|
|
+import com.adealink.frame.log.Log
|
|
|
+import com.adealink.frame.mvvm.view.viewBinding
|
|
|
+import com.adealink.frame.util.getPathFromUri
|
|
|
+import com.adealink.frame.util.runOnUiThread
|
|
|
+import com.adealink.weparty.commonui.ext.gone
|
|
|
+import com.adealink.weparty.commonui.ext.show
|
|
|
+import com.adealink.weparty.commonui.recycleview.adapter.ExtMultiTypeAdapter
|
|
|
+import com.adealink.weparty.commonui.toast.util.showFailedToast
|
|
|
+import com.adealink.weparty.commonui.widget.BottomDialogFragment
|
|
|
+import com.adealink.weparty.module.music.TAG_MUSIC
|
|
|
+import com.adealink.weparty.module.music.data.MusicItem
|
|
|
+import com.adealink.weparty.music.ISearchMusicFragment
|
|
|
+import com.adealink.weparty.music.R
|
|
|
+import com.adealink.weparty.music.databinding.DialogLocalMusicBinding
|
|
|
+import com.adealink.weparty.music.databinding.FragmentLocalMusicBinding
|
|
|
+import com.adealink.weparty.music.listener.OnMusicItemListener
|
|
|
+import com.adealink.weparty.music.local.adapter.LocalMusicItemViewBinder
|
|
|
+import com.adealink.weparty.music.viewmodel.MusicViewModel
|
|
|
+
|
|
|
+class LocalMusicDialog : BottomDialogFragment(R.layout.dialog_local_music),
|
|
|
+ ISearchMusicFragment,
|
|
|
+ OnMusicItemListener {
|
|
|
+
|
|
|
+ companion object {
|
|
|
+
|
|
|
+ fun newInstance(): LocalMusicDialog {
|
|
|
+ return LocalMusicDialog()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private val binding by viewBinding(DialogLocalMusicBinding::bind)
|
|
|
+ private val musicViewModel by viewModels<MusicViewModel>({ requireActivity() })
|
|
|
+ private val adapter by fastLazy { ExtMultiTypeAdapter() }
|
|
|
+ private var scanAnim: ValueAnimator? = null
|
|
|
+ override var searchMode: Boolean = false
|
|
|
+
|
|
|
+ override fun initViews() {
|
|
|
+ super.initViews()
|
|
|
+ binding.backBtn.setOnClickListener {
|
|
|
+ dismiss()
|
|
|
+ }
|
|
|
+ binding.scanBtn.setOnClickListener {
|
|
|
+ startScanAnim()
|
|
|
+
|
|
|
+ openFilePicker()
|
|
|
+ }
|
|
|
+ binding.musicList.itemAnimator = null
|
|
|
+ binding.musicList.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
|
|
+ adapter.register(LocalMusicItemViewBinder(this))
|
|
|
+ binding.musicList.adapter = adapter
|
|
|
+ }
|
|
|
+
|
|
|
+ private val filePickerLauncher =
|
|
|
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
|
|
+ if (result.resultCode == RESULT_OK) {
|
|
|
+ val data = result.data
|
|
|
+ if (data != null) {
|
|
|
+ handleFilePickerResult(data)
|
|
|
+ }
|
|
|
+ return@registerForActivityResult
|
|
|
+ }
|
|
|
+
|
|
|
+ musicViewModel.getLocalMusic(true).observe(viewLifecycleOwner) {
|
|
|
+ stopScanAnim()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun openFilePicker() {
|
|
|
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
|
|
+ intent.setType("audio/*")
|
|
|
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
|
|
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
|
|
|
+ filePickerLauncher.launch(intent)
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun handleFilePickerResult(data: Intent) {
|
|
|
+ val uris: MutableList<Uri> = ArrayList()
|
|
|
+ if (data.clipData != null) {
|
|
|
+ val count = data.clipData!!.itemCount
|
|
|
+ for (i in 0 until count) {
|
|
|
+ val fileUri = data.clipData!!.getItemAt(i).uri
|
|
|
+ uris.add(fileUri)
|
|
|
+ }
|
|
|
+ } else if (data.data != null) {
|
|
|
+ val fileUri = data.data ?: return
|
|
|
+ uris.add(fileUri)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 扫描选定的文件
|
|
|
+ scanFiles(uris.toList())
|
|
|
+ }
|
|
|
+
|
|
|
+ @OptIn(UnstableApi::class)
|
|
|
+ private fun scanFiles(uris: List<Uri>) {
|
|
|
+ val paths = arrayOfNulls<String>(uris.size)
|
|
|
+ for (i in uris.indices) {
|
|
|
+ val uri = uris[i]
|
|
|
+ Log.v(TAG_MUSIC, "scanFile: $uri")
|
|
|
+ val path: String = getPathFromUri(requireContext(), uri).toString()
|
|
|
+ paths[i] = path
|
|
|
+ }
|
|
|
+ var count = 0
|
|
|
+ MediaScannerConnection.scanFile(
|
|
|
+ context,
|
|
|
+ paths,
|
|
|
+ arrayOf(MimeTypes.normalizeMimeType(BASE_TYPE_AUDIO))
|
|
|
+ ) { path, uri ->
|
|
|
+ Log.v(TAG_MUSIC, "onScanCompleted: $path $uri")
|
|
|
+ count++
|
|
|
+ if (count == paths.size) {
|
|
|
+ runOnUiThread {
|
|
|
+ if (!isViewBindingValid()) {
|
|
|
+ return@runOnUiThread
|
|
|
+ }
|
|
|
+ musicViewModel.getLocalMusic(true).observe(viewLifecycleOwner) {
|
|
|
+ stopScanAnim()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun observeViewModel() {
|
|
|
+ super.observeViewModel()
|
|
|
+ musicViewModel.localMusicLD.observe(viewLifecycleOwner) {
|
|
|
+ updateList(it)
|
|
|
+ }
|
|
|
+ musicViewModel.localMusicItemChangedLD.observe(viewLifecycleOwner) {
|
|
|
+ adapter.notifyItemChanged(it) { old, new -> old.audioId == new.audioId }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun loadData() {
|
|
|
+ super.loadData()
|
|
|
+ musicViewModel.getLocalMusic(false)
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onAddClick(item: MusicItem) {
|
|
|
+ super.onAddClick(item)
|
|
|
+ musicViewModel.addToMyMusic(item).observe(viewLifecycleOwner) {
|
|
|
+ showFailedToast(it)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onRemoveClick(item: MusicItem) {
|
|
|
+ super.onRemoveClick(item)
|
|
|
+ musicViewModel.removeFromMyMusic(item)
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun showEmpty() {
|
|
|
+ binding.musicList.visibility = View.GONE
|
|
|
+ binding.emptyView.visibility = View.VISIBLE
|
|
|
+ binding.emptyView.show(com.adealink.weparty.R.drawable.common_list_empty_ic, R.string.music_empty)
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun showContent() {
|
|
|
+ binding.musicList.visibility = View.VISIBLE
|
|
|
+ binding.emptyView.visibility = View.GONE
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun startScanAnim() {
|
|
|
+ if (!isViewBindingValid()) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (scanAnim == null) {
|
|
|
+ scanAnim = ObjectAnimator.ofFloat(binding.scanIcon, "rotation", 0f, 360f)
|
|
|
+ scanAnim?.duration = 200
|
|
|
+ scanAnim?.repeatMode = ValueAnimator.RESTART
|
|
|
+ scanAnim?.repeatCount = ValueAnimator.INFINITE
|
|
|
+ scanAnim?.interpolator = LinearInterpolator()
|
|
|
+ }
|
|
|
+ scanAnim?.start()
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun stopScanAnim() {
|
|
|
+ scanAnim?.end()
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun clearAdapter() {
|
|
|
+ updateList(arrayListOf())
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun updateList(list: List<MusicItem>) {
|
|
|
+ if (list.isEmpty()) {
|
|
|
+ showEmpty()
|
|
|
+ } else {
|
|
|
+ showContent()
|
|
|
+ }
|
|
|
+ adapter.items = list
|
|
|
+ adapter.notifyDataSetChanged()
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun search(keyword: String) {
|
|
|
+ if (!isViewCreated()) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!searchMode) {
|
|
|
+ clearAdapter()
|
|
|
+ }
|
|
|
+ searchMode = true
|
|
|
+ musicViewModel.searchLocalMusic(keyword).observe(viewLifecycleOwner) {
|
|
|
+ if (!searchMode) {
|
|
|
+ return@observe
|
|
|
+ }
|
|
|
+
|
|
|
+ if (it is Rlt.Success) {
|
|
|
+ updateList(it.data)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun finishSearch() {
|
|
|
+ if (!searchMode) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ searchMode = false
|
|
|
+ if (!isViewCreated()) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ clearAdapter()
|
|
|
+ musicViewModel.getLocalMusic(false)
|
|
|
+ }
|
|
|
+
|
|
|
+}
|