diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cc2624a6..31f9fc84 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -89,9 +89,6 @@ dependencies { implementation("com.github.Kyant0:taglib:1.0.0-alpha22") - //TODO : use fetch instead of download manager -// implementation("com.github.tonyofrancis.Fetch:xfetch2:3.1.6") - testImplementation("org.jetbrains.kotlin:kotlin-reflect:1.9.24") testImplementation("junit:junit:4.13.2") } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/download/Downloader.kt b/app/src/main/java/dev/brahmkshatriya/echo/download/Downloader.kt index 613530fd..30903517 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/download/Downloader.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/download/Downloader.kt @@ -98,7 +98,7 @@ class Downloader( val folder = "Echo${parent?.title?.let { "/$it" } ?: ""}" val id = when (val audio = media.audio) { - is Streamable.Audio.ByteStream -> TODO() + is Streamable.Audio.ByteStream -> throw Exception("Not Supported") is Streamable.Audio.Channel -> TODO() is Streamable.Audio.Http -> { val request = audio.request diff --git a/app/src/main/java/dev/brahmkshatriya/echo/offline/OfflineExtension.kt b/app/src/main/java/dev/brahmkshatriya/echo/offline/OfflineExtension.kt index d0140586..95f9e74e 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/offline/OfflineExtension.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/offline/OfflineExtension.kt @@ -5,11 +5,11 @@ import androidx.media3.common.MediaItem import dev.brahmkshatriya.echo.R import dev.brahmkshatriya.echo.common.clients.AlbumClient import dev.brahmkshatriya.echo.common.clients.ArtistClient -import dev.brahmkshatriya.echo.common.clients.EditPlayerListenerClient import dev.brahmkshatriya.echo.common.clients.ExtensionClient import dev.brahmkshatriya.echo.common.clients.HomeFeedClient import dev.brahmkshatriya.echo.common.clients.LibraryClient import dev.brahmkshatriya.echo.common.clients.PlaylistClient +import dev.brahmkshatriya.echo.common.clients.PlaylistEditorListenerClient import dev.brahmkshatriya.echo.common.clients.RadioClient import dev.brahmkshatriya.echo.common.clients.SearchClient import dev.brahmkshatriya.echo.common.clients.TrackClient @@ -47,7 +47,7 @@ import dev.brahmkshatriya.echo.utils.toJson class OfflineExtension(val context: Context) : ExtensionClient, HomeFeedClient, TrackClient, AlbumClient, ArtistClient, PlaylistClient, RadioClient, SearchClient, LibraryClient, - EditPlayerListenerClient { + PlaylistEditorListenerClient { companion object { val metadata = ExtensionMetadata( diff --git a/app/src/main/java/dev/brahmkshatriya/echo/offline/TestExtension.kt b/app/src/main/java/dev/brahmkshatriya/echo/offline/TestExtension.kt index dde9f200..c34fbb99 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/offline/TestExtension.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/offline/TestExtension.kt @@ -136,77 +136,8 @@ class TestExtension : ExtensionClient, LoginClient.UsernamePassword, TrackClient return PagedData.Single { emptyList() } } - override suspend fun listEditablePlaylists(): List { - return listOf() - } - override suspend fun likeTrack(track: Track, liked: Boolean): Boolean { println("likeTrack: ${track.title}, $liked") return liked } - - override suspend fun createPlaylist(title: String, description: String?): Playlist { - return Playlist("", title, true) - } - - override suspend fun deletePlaylist(playlist: Playlist) {} - - override suspend fun editPlaylistMetadata( - playlist: Playlist, - title: String, - description: String? - ) { - } - - override suspend fun addTracksToPlaylist( - playlist: Playlist, - tracks: List, - index: Int, - new: List - ) { - } - - override suspend fun removeTracksFromPlaylist( - playlist: Playlist, - tracks: List, - indexes: List - ) { - } - - override suspend fun moveTrackInPlaylist( - playlist: Playlist, - tracks: List, - fromIndex: Int, - toIndex: Int - ) { - } - - override suspend fun loadPlaylist(playlist: Playlist) = playlist - override fun loadTracks(playlist: Playlist): PagedData = PagedData.Single { - listOf( - Track( - "track", - "Track", - emptyList(), - null, - null, - streamables = listOf(Streamable.audioVideo(video, 0)) - ) - ) - } - - override fun loadTracks(album: Album): PagedData { - return PagedData.Single { emptyList() } - } - - override fun getMediaItems(playlist: Playlist): PagedData = - PagedData.Single { listOf() } - - override fun getMediaItems(album: Album): PagedData { - return PagedData.Single { emptyList() } - } - - override suspend fun loadAlbum(album: Album): Album { - return album - } } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapter/MediaContainerAdapter.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapter/MediaContainerAdapter.kt index 156007d8..9bdcb053 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapter/MediaContainerAdapter.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapter/MediaContainerAdapter.kt @@ -5,6 +5,8 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.ViewModel import androidx.paging.LoadState import androidx.paging.PagingData @@ -80,6 +82,10 @@ class MediaContainerAdapter( } } + suspend fun submit(pagingData: PagingData?) { + saveState() + submitData(pagingData ?: PagingData.empty()) + } //Nested RecyclerView State Management @@ -101,6 +107,13 @@ class MediaContainerAdapter( stateViewModel.visibleScrollableViews.clear() } + private fun saveScrollState(holder: Category, block: ((Category) -> Unit)? = null) { + val layoutManagerStates = stateViewModel.layoutManagerStates + layoutManagerStates[holder.bindingAdapterPosition] = + holder.layoutManager?.onSaveInstanceState() + block?.invoke(holder) + } + private fun saveState() { stateViewModel.visibleScrollableViews.values.forEach { item -> item.get()?.let { saveScrollState(it) } @@ -110,24 +123,22 @@ class MediaContainerAdapter( override fun onViewRecycled(holder: MediaContainerViewHolder) { super.onViewRecycled(holder) + destroyLifeCycle(holder) if (holder is Category) saveScrollState(holder) { stateViewModel.visibleScrollableViews.remove(holder.bindingAdapterPosition) } } - private fun saveScrollState(holder: Category, block: ((Category) -> Unit)? = null) { - val layoutManagerStates = stateViewModel.layoutManagerStates - layoutManagerStates[holder.bindingAdapterPosition] = - holder.layoutManager?.onSaveInstanceState() - block?.invoke(holder) - } - - suspend fun submit(pagingData: PagingData?) { - saveState() - submitData(pagingData ?: PagingData.empty()) + private fun destroyLifeCycle(holder: MediaContainerViewHolder) { + if (holder.isInitialized && holder.lifecycleRegistry.currentState.isAtLeast(Lifecycle.State.STARTED)) + holder.lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) } override fun onBindViewHolder(holder: MediaContainerViewHolder, position: Int) { + destroyLifeCycle(holder) + holder.lifecycleRegistry = LifecycleRegistry(holder) + holder.lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) + val items = getItem(position) ?: return holder.transitionView.transitionName = (transition + items.id).hashCode().toString() holder.bind(items) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapter/MediaContainerViewHolder.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapter/MediaContainerViewHolder.kt index 9029375b..b577074e 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapter/MediaContainerViewHolder.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapter/MediaContainerViewHolder.kt @@ -5,6 +5,9 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.lifecycleScope import androidx.paging.PagingData import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -18,18 +21,20 @@ import dev.brahmkshatriya.echo.databinding.NewItemMediaBinding import dev.brahmkshatriya.echo.databinding.NewItemTracksBinding import dev.brahmkshatriya.echo.ui.adapter.MediaItemViewHolder.Companion.bind import dev.brahmkshatriya.echo.ui.item.TrackAdapter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.lang.ref.WeakReference sealed class MediaContainerViewHolder( itemView: View, -) : RecyclerView.ViewHolder(itemView) { +) : RecyclerView.ViewHolder(itemView), LifecycleOwner { abstract fun bind(container: MediaItemsContainer) open val clickView = itemView abstract val transitionView: View + lateinit var lifecycleRegistry : LifecycleRegistry + val isInitialized get() = this::lifecycleRegistry.isInitialized + override val lifecycle get() = lifecycleRegistry + class Category( val binding: NewItemCategoryBinding, val viewModel: MediaContainerAdapter.StateViewModel, @@ -159,10 +164,8 @@ sealed class MediaContainerViewHolder( val binding: NewItemTracksBinding, private val sharedPool: RecyclerView.RecycledViewPool, private val clientId: String?, - val listener: MediaItemAdapter.Listener, - val scope: CoroutineScope, - ) : - MediaContainerViewHolder(binding.root) { + val listener: MediaItemAdapter.Listener + ) : MediaContainerViewHolder(binding.root) { private val trackListener = object : TrackAdapter.Listener { override fun onClick(list: List, position: Int, view: View) { @@ -186,7 +189,7 @@ sealed class MediaContainerViewHolder( binding.recyclerView.adapter = adapter binding.recyclerView.setRecycledViewPool(sharedPool) binding.more.isVisible = tracks.more != null - scope.launch { adapter.submit(PagingData.from(tracks.list.take(6))) } + lifecycleScope.launch { adapter.submit(PagingData.from(tracks.list.take(6))) } } val layoutManager get() = binding.recyclerView.layoutManager @@ -201,14 +204,11 @@ sealed class MediaContainerViewHolder( listener: MediaItemAdapter.Listener, ): MediaContainerViewHolder { val layoutInflater = LayoutInflater.from(parent.context) - //TODO - val scope = CoroutineScope(Dispatchers.Main) return MediaTracks( NewItemTracksBinding.inflate(layoutInflater, parent, false), sharedPool, clientId, - listener, - scope + listener ) } } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/editplaylist/AddToPlaylistViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/editplaylist/AddToPlaylistViewModel.kt index 2a1384ee..23d8b3b2 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/editplaylist/AddToPlaylistViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/editplaylist/AddToPlaylistViewModel.kt @@ -3,7 +3,8 @@ package dev.brahmkshatriya.echo.ui.editplaylist import android.app.Application import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import dev.brahmkshatriya.echo.common.clients.LibraryClient +import dev.brahmkshatriya.echo.common.clients.AlbumClient +import dev.brahmkshatriya.echo.common.clients.PlaylistEditClient import dev.brahmkshatriya.echo.common.models.EchoMediaItem import dev.brahmkshatriya.echo.common.models.Playlist import dev.brahmkshatriya.echo.common.models.Track @@ -33,12 +34,14 @@ class AddToPlaylistViewModel @Inject constructor( override fun onInitialize() { val extension = extensionListFlow.getExtension(clientId) ?: return val client = extension.client - if (client !is LibraryClient) return + if (client !is PlaylistEditClient) return viewModelScope.launch(Dispatchers.IO) { tryWith(extension.info) { when (val mediaItem = item) { - is EchoMediaItem.Lists.AlbumItem -> + is EchoMediaItem.Lists.AlbumItem -> { + if(client !is AlbumClient) return@tryWith tracks.addAll(client.loadTracks(mediaItem.album).loadAll()) + } is EchoMediaItem.Lists.PlaylistItem -> tracks.addAll(client.loadTracks(mediaItem.playlist).loadAll()) is EchoMediaItem.TrackItem -> tracks.add(mediaItem.track) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/editplaylist/EditPlaylistFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/editplaylist/EditPlaylistFragment.kt index 4bb64e01..290d20e5 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/editplaylist/EditPlaylistFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/editplaylist/EditPlaylistFragment.kt @@ -15,7 +15,7 @@ import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import dev.brahmkshatriya.echo.R -import dev.brahmkshatriya.echo.common.clients.EditPlaylistCoverClient +import dev.brahmkshatriya.echo.common.clients.PlaylistEditCoverClient import dev.brahmkshatriya.echo.common.models.Playlist import dev.brahmkshatriya.echo.common.models.Track import dev.brahmkshatriya.echo.databinding.FragmentEditPlaylistBinding @@ -165,7 +165,7 @@ class EditPlaylistFragment : Fragment() { observe(viewModel.extensionListFlow) { viewModel.load(clientId, playlist) val client = viewModel.extensionListFlow.getExtension(clientId)?.client - header.showCover(client is EditPlaylistCoverClient) + header.showCover(client is PlaylistEditCoverClient) } binding.toolbar.setOnMenuItemClickListener { diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/editplaylist/EditPlaylistViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/editplaylist/EditPlaylistViewModel.kt index c7c9b3db..4cd3ed5b 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/editplaylist/EditPlaylistViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/editplaylist/EditPlaylistViewModel.kt @@ -5,9 +5,9 @@ import android.content.Context import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dev.brahmkshatriya.echo.R -import dev.brahmkshatriya.echo.common.clients.EditPlayerListenerClient -import dev.brahmkshatriya.echo.common.clients.EditPlaylistCoverClient -import dev.brahmkshatriya.echo.common.clients.LibraryClient +import dev.brahmkshatriya.echo.common.clients.PlaylistEditClient +import dev.brahmkshatriya.echo.common.clients.PlaylistEditCoverClient +import dev.brahmkshatriya.echo.common.clients.PlaylistEditorListenerClient import dev.brahmkshatriya.echo.common.models.Playlist import dev.brahmkshatriya.echo.common.models.Track import dev.brahmkshatriya.echo.plugger.MusicExtension @@ -39,7 +39,7 @@ class EditPlaylistViewModel @Inject constructor( loadingFlow.emit(true) val extension = extensionListFlow.getExtension(clientId) val client = extension?.client - if (client !is LibraryClient) return@launch + if (client !is PlaylistEditClient) return@launch withContext(Dispatchers.IO) { originalList = tryWith(extension.info) { client.loadTracks(playlist).loadAll() } ?: emptyList() @@ -50,8 +50,8 @@ class EditPlaylistViewModel @Inject constructor( } } - private suspend fun libraryClient( - clientId: String, block: suspend (client: LibraryClient) -> Unit + private suspend fun playlistEditClient( + clientId: String, block: suspend (client: PlaylistEditClient) -> Unit ) { client(clientId, block) } @@ -68,7 +68,7 @@ class EditPlaylistViewModel @Inject constructor( fun changeMetadata(clientId: String, playlist: Playlist, title: String, description: String?) { viewModelScope.launch { - libraryClient(clientId) { + playlistEditClient(clientId) { it.editPlaylistMetadata(playlist, title, description) } } @@ -76,7 +76,7 @@ class EditPlaylistViewModel @Inject constructor( fun changeCover(clientId: String, playlist: Playlist, file: File?) { viewModelScope.launch { - client(clientId) { + client(clientId) { it.editPlaylistCover(playlist, file) } } @@ -133,10 +133,10 @@ class EditPlaylistViewModel @Inject constructor( val tracks = originalList.toMutableList() performedActions.emit(tracks to null) println("Actions Size : ${newActions.size}") - client(clientId) { + client(clientId) { it.onEnterPlaylistEditor(playlist, tracks) } - libraryClient(clientId) { client -> + playlistEditClient(clientId) { client -> newActions.forEach { action -> println("new action : $action") performedActions.emit(tracks to action) @@ -161,7 +161,7 @@ class EditPlaylistViewModel @Inject constructor( } } performedActions.emit(tracks to null) - client(clientId) { + client(clientId) { it.onExitPlaylistEditor(playlist, tracks) } } @@ -193,7 +193,7 @@ class EditPlaylistViewModel @Inject constructor( ) { val extension = extensionListFlow.getExtension(clientId) ?: return val client = extension.client - if (client !is LibraryClient) return + if (client !is PlaylistEditClient) return viewModelScope.launch(Dispatchers.IO) { tryWith(extension.info) { println("deleting playlist : ${playlist.title}") @@ -214,8 +214,8 @@ class EditPlaylistViewModel @Inject constructor( ) = run { val extension = extensionListFlow.getExtension(clientId) ?: return val client = extension.client - if (client !is LibraryClient) return - val listener = client as? EditPlayerListenerClient + if (client !is PlaylistEditClient) return + val listener = client as? PlaylistEditorListenerClient withContext(Dispatchers.IO) { playlists.forEach { playlist -> tryWith(extension.info) { diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemBottomSheet.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemBottomSheet.kt index 97a1e5b8..1a486ca8 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemBottomSheet.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemBottomSheet.kt @@ -15,6 +15,7 @@ import dev.brahmkshatriya.echo.R import dev.brahmkshatriya.echo.common.clients.ArtistFollowClient import dev.brahmkshatriya.echo.common.clients.LibraryClient import dev.brahmkshatriya.echo.common.clients.RadioClient +import dev.brahmkshatriya.echo.common.clients.SaveToLibraryClient import dev.brahmkshatriya.echo.common.clients.ShareClient import dev.brahmkshatriya.echo.common.clients.TrackClient import dev.brahmkshatriya.echo.common.models.EchoMediaItem @@ -31,7 +32,7 @@ import dev.brahmkshatriya.echo.ui.editplaylist.AddToPlaylistBottomSheet import dev.brahmkshatriya.echo.ui.exception.ExceptionFragment.Companion.copyToClipboard import dev.brahmkshatriya.echo.utils.autoCleared import dev.brahmkshatriya.echo.utils.getSerialized -import dev.brahmkshatriya.echo.utils.loadInto +import dev.brahmkshatriya.echo.utils.loadWith import dev.brahmkshatriya.echo.utils.observe import dev.brahmkshatriya.echo.utils.putSerialized import dev.brahmkshatriya.echo.viewmodels.DownloadViewModel @@ -139,12 +140,20 @@ class ItemBottomSheet : BottomSheetDialogFragment() { listOfNotNull( if (client is LibraryClient) ItemAction.Resource( - R.drawable.ic_bookmark_outline, R.string.save_to_playlist + R.drawable.ic_library_music, R.string.save_to_playlist ) { AddToPlaylistBottomSheet.newInstance(clientId, item) .show(parentFragmentManager, null) } else null, + if (client is SaveToLibraryClient) { + if (loaded) ItemAction.Resource( + R.drawable.ic_bookmark_filled, R.string.remove_from_library + ) { viewModel.removeFromLibrary(item) } + else ItemAction.Resource( + R.drawable.ic_bookmark_outline, R.string.save_to_library + ) { viewModel.saveToLibrary(item) } + } else null, if (client is RadioClient) ItemAction.Resource(R.drawable.ic_sensors, R.string.radio) { playerViewModel.radio(clientId, item) @@ -166,12 +175,20 @@ class ItemBottomSheet : BottomSheetDialogFragment() { else null, if (client is LibraryClient) ItemAction.Resource( - R.drawable.ic_bookmark_outline, R.string.save_to_playlist + R.drawable.ic_library_music, R.string.save_to_playlist ) { AddToPlaylistBottomSheet.newInstance(clientId, item) .show(parentFragmentManager, null) } else null, + if (client is SaveToLibraryClient) { + if (loaded) ItemAction.Resource( + R.drawable.ic_bookmark_filled, R.string.remove_from_library + ) { viewModel.removeFromLibrary(item) } + else ItemAction.Resource( + R.drawable.ic_bookmark_outline, R.string.save_to_library + ) { viewModel.saveToLibrary(item) } + } else null, if (client is LibraryClient && item.playlist.isEditable) ItemAction.Resource(R.drawable.ic_delete, R.string.delete_playlist) { playerViewModel.deletePlaylist(clientId, item.playlist) @@ -195,6 +212,14 @@ class ItemBottomSheet : BottomSheetDialogFragment() { playerViewModel.radio(clientId, item) } else null, + if (client is SaveToLibraryClient) { + if (loaded) ItemAction.Resource( + R.drawable.ic_bookmark_filled, R.string.remove_from_library + ) { viewModel.removeFromLibrary(item) } + else ItemAction.Resource( + R.drawable.ic_bookmark_outline, R.string.save_to_library + ) { viewModel.saveToLibrary(item) } + } else null, if (client is ArtistFollowClient) if (!item.artist.isFollowing) ItemAction.Resource(R.drawable.ic_heart_outline, R.string.follow) { @@ -238,11 +263,19 @@ class ItemBottomSheet : BottomSheetDialogFragment() { } else null, if (client is LibraryClient) - ItemAction.Resource(R.drawable.ic_bookmark_outline, R.string.save_to_playlist) { + ItemAction.Resource(R.drawable.ic_library_music, R.string.save_to_playlist) { AddToPlaylistBottomSheet.newInstance(clientId, item) .show(parentFragmentManager, null) } else null, + if (client is SaveToLibraryClient) { + if (loaded) ItemAction.Resource( + R.drawable.ic_bookmark_filled, R.string.remove_from_library + ) { viewModel.removeFromLibrary(item) } + else ItemAction.Resource( + R.drawable.ic_bookmark_outline, R.string.save_to_library + ) { viewModel.saveToLibrary(item) } + } else null, if (client is RadioClient) ItemAction.Resource(R.drawable.ic_sensors, R.string.radio) { playerViewModel.radio(clientId, item) @@ -320,8 +353,12 @@ class ItemBottomSheet : BottomSheetDialogFragment() { is ItemAction.Custom -> { binding.textView.text = action.title - binding.imageView.imageTintList = colorState - action.image.loadInto(binding.imageView, action.placeholder) + action.image.loadWith(binding.root) { + if(it == null) { + binding.imageView.imageTintList = colorState + binding.imageView.setImageResource(action.placeholder) + } else binding.imageView.setImageDrawable(it) + } } } } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemViewModel.kt index 57ec242f..4b34d693 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemViewModel.kt @@ -1,14 +1,17 @@ package dev.brahmkshatriya.echo.ui.item +import android.app.Application import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import dagger.hilt.android.lifecycle.HiltViewModel +import dev.brahmkshatriya.echo.R import dev.brahmkshatriya.echo.common.clients.AlbumClient import dev.brahmkshatriya.echo.common.clients.ArtistClient import dev.brahmkshatriya.echo.common.clients.ArtistFollowClient import dev.brahmkshatriya.echo.common.clients.PlaylistClient import dev.brahmkshatriya.echo.common.clients.RadioClient +import dev.brahmkshatriya.echo.common.clients.SaveToLibraryClient import dev.brahmkshatriya.echo.common.clients.TrackClient import dev.brahmkshatriya.echo.common.clients.UserClient import dev.brahmkshatriya.echo.common.helpers.PagedData @@ -16,6 +19,12 @@ import dev.brahmkshatriya.echo.common.models.Album import dev.brahmkshatriya.echo.common.models.Artist import dev.brahmkshatriya.echo.common.models.EchoMediaItem import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Companion.toMediaItem +import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Lists.AlbumItem +import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Lists.PlaylistItem +import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Lists.RadioItem +import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Profile.ArtistItem +import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Profile.UserItem +import dev.brahmkshatriya.echo.common.models.EchoMediaItem.TrackItem import dev.brahmkshatriya.echo.common.models.MediaItemsContainer import dev.brahmkshatriya.echo.common.models.Playlist import dev.brahmkshatriya.echo.common.models.Radio @@ -36,6 +45,7 @@ import javax.inject.Inject class ItemViewModel @Inject constructor( throwableFlow: MutableSharedFlow, val extensionListFlow: MutableStateFlow?>, + val app: Application, ) : CatchingViewModel(throwableFlow) { var item: EchoMediaItem? = null @@ -51,31 +61,78 @@ class ItemViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { itemFlow.value = null val mediaItem = when (val item = item!!) { - is EchoMediaItem.Lists.AlbumItem -> getClient { - load(it, item.album, ::loadAlbum, ::getMediaItems)?.toMediaItem() - } - - is EchoMediaItem.Lists.PlaylistItem -> getClient { - load(it, item.playlist, ::loadPlaylist, ::getMediaItems)?.toMediaItem() - } - - is EchoMediaItem.Profile.ArtistItem -> getClient { - load(it, item.artist, ::loadArtist, ::getMediaItems)?.toMediaItem() - } + is AlbumItem -> loadItem( + item, { loadAlbum(it.album).toMediaItem() }, { getMediaItems(it.album) } + ) + + is PlaylistItem -> loadItem( + item, + { loadPlaylist(it.playlist).toMediaItem() }, + { getMediaItems(it.playlist) } + ) + + is ArtistItem -> loadItem( + item, { loadArtist(it.artist).toMediaItem() }, { getMediaItems(it.artist) } + ) + + is UserItem -> loadItem( + item, { loadUser(it.user).toMediaItem() }, { getMediaItems(it.user) } + ) + + is TrackItem -> loadItem( + item, { loadTrack(it.track).toMediaItem() }, { trackItem -> + val client = this + val track = trackItem.track + val album = trackItem.track.album + val artists = trackItem.track.artists + PagedData.Concat( + if (client is AlbumClient && album != null) PagedData.Single { + listOf( + client.loadAlbum(album).toMediaItem().toMediaItemsContainer() + ) + } else PagedData.empty(), + if (artists.isNotEmpty()) PagedData.Single { + listOf( + MediaItemsContainer.Category( + app.getString(R.string.artists), + if (client is ArtistClient) artists.map { + val artist = client.loadArtist(it) + artist.toMediaItem() + } else artists.map { it.toMediaItem() } + ) + ) + } else PagedData.empty(), + client.getMediaItems(track) + ) + } + ) + + is RadioItem -> loadItem(item, { it }, { null }) + } + itemFlow.value = mediaItem + } + } - is EchoMediaItem.Profile.UserItem -> getClient { - load(it, item.user, ::loadUser, ::getMediaItems)?.toMediaItem() - } + val savedState = MutableStateFlow(false) + private suspend inline fun loadItem( + item: T, + crossinline loadItem: suspend U.(T) -> T, + crossinline loadRelated: U.(T) -> PagedData? = { null } + ): T? { + return getClient { info -> + tryWith(info) { + val loaded = loadItem(item) - is EchoMediaItem.TrackItem -> getClient { - load(it, item.track, ::loadTrack, ::getMediaItems)?.toMediaItem() - } + if (this is SaveToLibraryClient) + savedState.value = isSavedToLibrary(loaded) - is EchoMediaItem.Lists.RadioItem -> getClient { - load(it, item.radio, null, null)?.toMediaItem() + viewModelScope.launch { + if (loadRelatedFeed) tryWith(info) { + loadRelated(loaded)?.toFlow()?.map { it } + }?.collectTo(relatedFeed) } + loaded } - itemFlow.value = mediaItem } } @@ -83,30 +140,13 @@ class ItemViewModel @Inject constructor( load() } - private inline fun getClient( + private inline fun getClient( block: T.(info: ExtensionInfo) -> R? ) = extension?.run { val client = client if (client is T) block(client, info) else null } - private suspend fun load( - info: ExtensionInfo, - item: T, - loadItem: (suspend (T) -> T)?, - loadRelated: ((T) -> PagedData)? - ): T? { - return tryWith(info) { - val loaded = loadItem?.let { it(item) } ?: item - if (loadRelatedFeed && loadRelated != null) viewModelScope.launch { - tryWith(info) { - loadRelated(loaded).toFlow().map { it } - }?.collectTo(relatedFeed) - } - loaded - } - } - private val songsFlow = MutableStateFlow?>(null) val songsLiveData = songsFlow.asLiveData() @@ -148,4 +188,20 @@ class ItemViewModel @Inject constructor( } } } + + fun removeFromLibrary(item: EchoMediaItem) { + viewModelScope.launch(Dispatchers.IO) { + getClient { + tryWith(it) { removeFromLibrary(item) } + } + } + } + + fun saveToLibrary(item: EchoMediaItem) { + viewModelScope.launch(Dispatchers.IO) { + getClient { + tryWith(it) { saveToLibrary(item) } + } + } + } } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/library/CreatePlaylistFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/library/CreatePlaylistFragment.kt index 26f9f852..596af3a3 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/library/CreatePlaylistFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/library/CreatePlaylistFragment.kt @@ -50,9 +50,4 @@ class CreatePlaylistFragment : Fragment() { viewModel.createPlaylist(title) parentFragmentManager.popBackStack() } - - override fun onDestroy() { - super.onDestroy() - //TODO Shift to Fragment Result - } } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/library/LibraryFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/library/LibraryFragment.kt index 5e572b27..f7893add 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/library/LibraryFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/library/LibraryFragment.kt @@ -10,6 +10,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import dev.brahmkshatriya.echo.R import dev.brahmkshatriya.echo.common.clients.LibraryClient +import dev.brahmkshatriya.echo.common.clients.PlaylistEditClient import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Companion.toMediaItem import dev.brahmkshatriya.echo.databinding.FragmentLibraryBinding import dev.brahmkshatriya.echo.ui.adapter.MediaContainerAdapter @@ -70,6 +71,9 @@ class LibraryFragment : Fragment() { binding.appBarOutline.alpha = 0f } + observe(viewModel.extensionFlow) { + binding.fabCreatePlaylist.isVisible = it?.client is PlaylistEditClient + } binding.fabCreatePlaylist.setOnClickListener { parent.openFragment(CreatePlaylistFragment(), it) } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/library/LibraryViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/library/LibraryViewModel.kt index 69f09683..9a0df273 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/library/LibraryViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dev.brahmkshatriya.echo.common.clients.ExtensionClient import dev.brahmkshatriya.echo.common.clients.LibraryClient +import dev.brahmkshatriya.echo.common.clients.PlaylistEditClient import dev.brahmkshatriya.echo.common.models.Playlist import dev.brahmkshatriya.echo.db.models.UserEntity import dev.brahmkshatriya.echo.plugger.MusicExtension @@ -31,7 +32,7 @@ class LibraryViewModel @Inject constructor( val playlistCreatedFlow = MutableSharedFlow>() fun createPlaylist(title: String) { val extension = extensionFlow.value ?: return - val client = extension.client as? LibraryClient ?: return + val client = extension.client as? PlaylistEditClient ?: return viewModelScope.launch(Dispatchers.IO) { val playlist = tryWith(extension.info) { client.createPlaylist(title, null) } ?: return@launch diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ede69325..679f5226 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -212,6 +212,8 @@ Permission Required Permission Denied This permission is required to access your music library + Remove from Library + Save to Library Highest Medium diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/LibraryClient.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/LibraryClient.kt index b31d3e1e..1dee75eb 100644 --- a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/LibraryClient.kt +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/LibraryClient.kt @@ -2,27 +2,11 @@ package dev.brahmkshatriya.echo.common.clients import dev.brahmkshatriya.echo.common.helpers.PagedData import dev.brahmkshatriya.echo.common.models.MediaItemsContainer -import dev.brahmkshatriya.echo.common.models.Playlist import dev.brahmkshatriya.echo.common.models.Tab import dev.brahmkshatriya.echo.common.models.Track -interface LibraryClient : PlaylistClient, AlbumClient { +interface LibraryClient { suspend fun getLibraryTabs(): List fun getLibraryFeed(tab: Tab?): PagedData - suspend fun listEditablePlaylists(): List suspend fun likeTrack(track: Track, liked: Boolean): Boolean - suspend fun createPlaylist(title: String, description: String?): Playlist - suspend fun deletePlaylist(playlist: Playlist) - suspend fun editPlaylistMetadata(playlist: Playlist, title: String, description: String?) - suspend fun addTracksToPlaylist( - playlist: Playlist, tracks: List, index: Int, new: List - ) - - suspend fun removeTracksFromPlaylist( - playlist: Playlist, tracks: List, indexes: List - ) - - suspend fun moveTrackInPlaylist( - playlist: Playlist, tracks: List, fromIndex: Int, toIndex: Int - ) } \ No newline at end of file diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/PlaylistEditClient.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/PlaylistEditClient.kt new file mode 100644 index 00000000..8b362b36 --- /dev/null +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/PlaylistEditClient.kt @@ -0,0 +1,25 @@ +package dev.brahmkshatriya.echo.common.clients + +import dev.brahmkshatriya.echo.common.models.Playlist +import dev.brahmkshatriya.echo.common.models.Track + +interface PlaylistEditClient : PlaylistClient { + + suspend fun listEditablePlaylists(): List + + suspend fun createPlaylist(title: String, description: String?): Playlist + suspend fun deletePlaylist(playlist: Playlist) + suspend fun editPlaylistMetadata(playlist: Playlist, title: String, description: String?) + + suspend fun addTracksToPlaylist( + playlist: Playlist, tracks: List, index: Int, new: List + ) + + suspend fun removeTracksFromPlaylist( + playlist: Playlist, tracks: List, indexes: List + ) + + suspend fun moveTrackInPlaylist( + playlist: Playlist, tracks: List, fromIndex: Int, toIndex: Int + ) +} \ No newline at end of file diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/EditPlaylistCoverClient.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/PlaylistEditCoverClient.kt similarity index 76% rename from common/src/main/java/dev/brahmkshatriya/echo/common/clients/EditPlaylistCoverClient.kt rename to common/src/main/java/dev/brahmkshatriya/echo/common/clients/PlaylistEditCoverClient.kt index 68b15683..8fb70296 100644 --- a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/EditPlaylistCoverClient.kt +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/PlaylistEditCoverClient.kt @@ -3,6 +3,6 @@ package dev.brahmkshatriya.echo.common.clients import dev.brahmkshatriya.echo.common.models.Playlist import java.io.File -interface EditPlaylistCoverClient { +interface PlaylistEditCoverClient : PlaylistEditClient { fun editPlaylistCover(playlist: Playlist, cover: File?) } \ No newline at end of file diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/PlaylistEditPrivacyClient.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/PlaylistEditPrivacyClient.kt new file mode 100644 index 00000000..61309dd0 --- /dev/null +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/PlaylistEditPrivacyClient.kt @@ -0,0 +1,8 @@ +package dev.brahmkshatriya.echo.common.clients + +import dev.brahmkshatriya.echo.common.models.Playlist + +//TODO +interface PlaylistEditPrivacyClient : PlaylistEditClient { + suspend fun setPrivacy(playlist: Playlist, isPrivate: Boolean) +} \ No newline at end of file diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/EditPlayerListenerClient.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/PlaylistEditorListenerClient.kt similarity index 83% rename from common/src/main/java/dev/brahmkshatriya/echo/common/clients/EditPlayerListenerClient.kt rename to common/src/main/java/dev/brahmkshatriya/echo/common/clients/PlaylistEditorListenerClient.kt index 804dbd79..f256a451 100644 --- a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/EditPlayerListenerClient.kt +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/PlaylistEditorListenerClient.kt @@ -3,7 +3,7 @@ package dev.brahmkshatriya.echo.common.clients import dev.brahmkshatriya.echo.common.models.Playlist import dev.brahmkshatriya.echo.common.models.Track -interface EditPlayerListenerClient { +interface PlaylistEditorListenerClient : PlaylistEditClient { suspend fun onEnterPlaylistEditor(playlist: Playlist, tracks: List) suspend fun onExitPlaylistEditor(playlist: Playlist, tracks: List) } \ No newline at end of file diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/SaveToLibraryClient.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/SaveToLibraryClient.kt index db7c5c6d..7833b6e5 100644 --- a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/SaveToLibraryClient.kt +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/SaveToLibraryClient.kt @@ -5,5 +5,5 @@ import dev.brahmkshatriya.echo.common.models.EchoMediaItem interface SaveToLibraryClient { suspend fun saveToLibrary(mediaItem: EchoMediaItem) suspend fun removeFromLibrary(mediaItem: EchoMediaItem) - suspend fun isSavedToLibrary(mediaItem: EchoMediaItem) + suspend fun isSavedToLibrary(mediaItem: EchoMediaItem) : Boolean } \ No newline at end of file diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/models/Playlist.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/models/Playlist.kt index 02edd35a..93165cc8 100644 --- a/common/src/main/java/dev/brahmkshatriya/echo/common/models/Playlist.kt +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/models/Playlist.kt @@ -14,5 +14,6 @@ data class Playlist( val duration: Long? = null, val description: String? = null, val subtitle: String? = null, - val extras: Map = mapOf() + val extras: Map = mapOf(), + val isPrivate: Boolean = true, ) \ No newline at end of file