diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryArtistFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryArtistFragment.kt index e0129f46..4c904eb7 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryArtistFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryArtistFragment.kt @@ -8,94 +8,52 @@ import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter -import android.widget.EditText import android.widget.FrameLayout import android.widget.ImageButton import android.widget.ImageView import android.widget.LinearLayout -import android.widget.LinearLayout.GONE -import android.widget.LinearLayout.VISIBLE -import android.widget.Spinner import android.widget.TextView import androidx.appcompat.widget.AppCompatImageView -import androidx.core.view.allViews -import androidx.core.widget.addTextChangedListener import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 -import com.bumptech.glide.Glide import com.futo.platformplayer.R import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UISlideOverlays -import com.futo.platformplayer.api.media.PlatformID -import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException import com.futo.platformplayer.api.media.models.PlatformAuthorLink -import com.futo.platformplayer.api.media.models.ResultCapabilities -import com.futo.platformplayer.api.media.models.channels.IPlatformChannel -import com.futo.platformplayer.api.media.models.channels.SerializedChannel import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist import com.futo.platformplayer.api.media.models.post.IPlatformPost import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo -import com.futo.platformplayer.api.media.platforms.local.LocalClient import com.futo.platformplayer.api.media.structures.AdhocPager import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.assume import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.constructs.Event3 -import com.futo.platformplayer.constructs.TaskHandler -import com.futo.platformplayer.dp -import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment -import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment -import com.futo.platformplayer.fragment.channel.tab.ChannelListFragment -import com.futo.platformplayer.fragment.channel.tab.ChannelMonetizationFragment -import com.futo.platformplayer.fragment.channel.tab.ChannelPlaylistsFragment -import com.futo.platformplayer.fragment.channel.tab.IChannelTabFragment -import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment.Companion +import com.futo.platformplayer.fragment.mainactivity.main.LibraryAlbumsFragment.AlbumViewHolder import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment -import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.SearchType -import com.futo.platformplayer.models.Subscription -import com.futo.platformplayer.selectBestImage -import com.futo.platformplayer.selectHighestResolutionImage import com.futo.platformplayer.states.Album import com.futo.platformplayer.states.Artist import com.futo.platformplayer.states.StateLibrary import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlaylists -import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StateSubscriptions -import com.futo.platformplayer.stores.FragmentedStorage -import com.futo.platformplayer.stores.StringStorage -import com.futo.platformplayer.toHumanNumber import com.futo.platformplayer.views.FeedStyle -import com.futo.platformplayer.views.ToggleBar -import com.futo.platformplayer.views.adapters.AnyAdapter -import com.futo.platformplayer.views.adapters.ChannelTab -import com.futo.platformplayer.views.adapters.ChannelViewPagerAdapter import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader -import com.futo.platformplayer.views.adapters.SubscriptionAdapter -import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay -import com.futo.platformplayer.views.platform.PlatformIndicator import com.futo.platformplayer.views.subscriptions.SubscribeButton -import com.futo.polycentric.core.ApiMethods -import com.futo.polycentric.core.PolycentricProfile -import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import kotlinx.coroutines.Dispatchers @@ -384,31 +342,14 @@ class LibraryArtistFragment : MainFragment() { var supportsPlaylists = false; val playlistPosition = 1 - if (supportsPlaylists && !(_viewPager.adapter as ArtistViewPagerAdapter).containsItem( - ArtistTab.PLAYLISTS.ordinal.toLong() - ) - ) { - // keep the current tab selected - if (_viewPager.currentItem >= playlistPosition) { - _viewPager.setCurrentItem(_viewPager.currentItem + 1, false) - } - - (_viewPager.adapter as ArtistViewPagerAdapter).insert( - playlistPosition, - ArtistTab.PLAYLISTS - ) - } - if (!supportsPlaylists && (_viewPager.adapter as ArtistViewPagerAdapter).containsItem( - ArtistTab.PLAYLISTS.ordinal.toLong() - ) - ) { - // keep the current tab selected - if (_viewPager.currentItem >= playlistPosition) { - _viewPager.setCurrentItem(_viewPager.currentItem - 1, false) - } - - (_viewPager.adapter as ArtistViewPagerAdapter).remove(playlistPosition) + // keep the current tab selected + if (_viewPager.currentItem >= playlistPosition) { + _viewPager.setCurrentItem(_viewPager.currentItem + 1, false) } + (_viewPager.adapter as ArtistViewPagerAdapter).insert( + playlistPosition, + ArtistTab.ALBUMS + ) // sets the channel for each tab for (fragment in _fragment.childFragmentManager.fragments) { @@ -429,15 +370,15 @@ class LibraryArtistFragment : MainFragment() { } } enum class ArtistTab { - VIDEOS, PLAYLISTS + SONGS, ALBUMS } class ArtistViewPagerAdapter(private val fragment: LibraryArtistFragment, fragmentManager: FragmentManager, lifecycle: Lifecycle) : FragmentStateAdapter(fragmentManager, lifecycle) { private val _supportedFragments = mutableMapOf( - ArtistTab.VIDEOS.ordinal to ArtistTab.VIDEOS + ArtistTab.SONGS.ordinal to ArtistTab.SONGS ) - private val _tabs = arrayListOf(ArtistTab.VIDEOS) + private val _tabs = arrayListOf(ArtistTab.SONGS, ArtistTab.ALBUMS) var artist: Artist? = null @@ -486,35 +427,15 @@ class LibraryArtistFragment : MainFragment() { override fun createFragment(position: Int): Fragment { val fragment: Fragment when (_tabs[position]) { - ArtistTab.VIDEOS -> { + ArtistTab.SONGS -> { fragment = ChannelContentsFragment(this.fragment).apply { - /* - onContentClicked.subscribe { video, num, _ -> - this@ArtistViewPagerAdapter.onContentClicked.emit(video, num) - } - onContentUrlClicked.subscribe(this@ArtistViewPagerAdapter.onContentUrlClicked::emit) - onUrlClicked.subscribe(this@ArtistViewPagerAdapter.onUrlClicked::emit) - onChannelClicked.subscribe(this@ArtistViewPagerAdapter.onChannelClicked::emit) - onAddToClicked.subscribe(this@ArtistViewPagerAdapter.onAddToClicked::emit) - onAddToQueueClicked.subscribe(this@ArtistViewPagerAdapter.onAddToQueueClicked::emit) - onAddToWatchLaterClicked.subscribe(this@ArtistViewPagerAdapter.onAddToWatchLaterClicked::emit) - onLongPress.subscribe(this@ArtistViewPagerAdapter.onLongPress::emit) - */ + } } - ArtistTab.PLAYLISTS -> { - fragment = ChannelPlaylistsFragment.newInstance().apply { - /* - onContentClicked.subscribe(this@ArtistViewPagerAdapter.onContentClicked::emit) - onContentUrlClicked.subscribe(this@ArtistViewPagerAdapter.onContentUrlClicked::emit) - onUrlClicked.subscribe(this@ArtistViewPagerAdapter.onUrlClicked::emit) - onChannelClicked.subscribe(this@ArtistViewPagerAdapter.onChannelClicked::emit) - onAddToClicked.subscribe(this@ArtistViewPagerAdapter.onAddToClicked::emit) - onAddToQueueClicked.subscribe(this@ArtistViewPagerAdapter.onAddToQueueClicked::emit) - onAddToWatchLaterClicked.subscribe(this@ArtistViewPagerAdapter.onAddToWatchLaterClicked::emit) - onLongPress.subscribe(this@ArtistViewPagerAdapter.onLongPress::emit) - */ + ArtistTab.ALBUMS -> { + fragment = ArtistAlbumsFragment(this.fragment).apply { + } } } @@ -569,6 +490,77 @@ class LibraryArtistFragment : MainFragment() { override fun updateSpanCount(){ } + companion object { + private const val TAG = "LibraryAlbumsFragmentsView"; + } + } + class ArtistAlbumsFragment(private val frag: LibraryArtistFragment) : Fragment(), IArtistTabFragment { + + var view: ArtistAlbumsView? = null; + + private var _lastArtist: Artist? = null; + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + view = ArtistAlbumsView(frag, inflater); + _lastArtist?.let { + view?.setArtist(it); + } + return view; + } + + override fun onDestroyView() { + view = null; + super.onDestroyView() + } + + override fun setArtist(artist: Artist) { + view?.setArtist(artist); + _lastArtist = artist; + } + } + class ArtistAlbumsView : FeedView, AlbumViewHolder> { + override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator; + + constructor(fragment: LibraryArtistFragment, inflater: LayoutInflater) : super(fragment, inflater) + + fun onShown() { + } + + fun setArtist(artist: Artist) { + val initialAlbums = artist.getAlbums(); + Logger.i(TAG, "Initial album count: " + initialAlbums.size); + + setPager(AdhocPager({ listOf() }, initialAlbums)); + } + + override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList): InsertedViewAdapterWithLoader { + return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(), + childCountGetter = { dataset.size }, + childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); }, + childViewHolderFactory = { viewGroup, _ -> + val holder = AlbumViewHolder(viewGroup); + holder.onClick.subscribe { c -> fragment.navigate(c) }; + return@InsertedViewAdapterWithLoader holder; + } + ); + } + + override fun updateSpanCount(){ } + + override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager { + val glmResults = GridLayoutManager(context, 1) + + _swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply { + rightMargin = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 8.0f, + context.resources.displayMetrics + ).toInt() + } + + return glmResults + } + companion object { private const val TAG = "LibraryAlbumsFragmentsView"; } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryArtistsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryArtistsFragment.kt index 44320f12..88d4ffdf 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryArtistsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryArtistsFragment.kt @@ -88,6 +88,19 @@ class LibraryArtistsFragment : MainFragment() { setPager(AdhocPager({ listOf(); }, intialArtists)); } + override fun reload() { + try { + setLoading(true); + val intialArtists = StateLibrary.instance.getArtists(); + Logger.i(TAG, "Initial album count: " + intialArtists.size); + + setPager(AdhocPager({ listOf(); }, intialArtists)); + } + finally { + setLoading(false); + } + } + override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList): InsertedViewAdapterWithLoader { return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(), childCountGetter = { dataset.size }, @@ -123,18 +136,18 @@ class LibraryArtistsFragment : MainFragment() { } class ArtistViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder( - LayoutInflater.from(_viewGroup.context).inflate(R.layout.list_album, + LayoutInflater.from(_viewGroup.context).inflate(R.layout.list_artist, _viewGroup, false)) { val onClick = Event1(); protected var _artist: Artist? = null; - protected val _imageThumbnail: ImageView + //protected val _imageThumbnail: ImageView protected val _textName: TextView protected val _textMetadata: TextView init { - _imageThumbnail = _view.findViewById(R.id.image_thumbnail); + //_imageThumbnail = _view.findViewById(R.id.image_thumbnail); _textName = _view.findViewById(R.id.text_name); _textMetadata = _view.findViewById(R.id.text_metadata); @@ -143,6 +156,7 @@ class LibraryArtistsFragment : MainFragment() { override fun bind(artist: Artist) { _artist = artist; + /* _imageThumbnail?.let { if (artist.thumbnail != null) Glide.with(it) @@ -152,6 +166,7 @@ class LibraryArtistsFragment : MainFragment() { else Glide.with(it).load(R.drawable.placeholder_channel_thumbnail).into(it); }; + */ _textName.text = artist.name; _textMetadata.text = artist.countTracks?.let { "${it} tracks" } ?: ""; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFragment.kt index 7a04c859..77f56d62 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFragment.kt @@ -7,10 +7,14 @@ import android.provider.MediaStore import android.util.AttributeSet import android.view.LayoutInflater import android.view.ViewGroup +import android.widget.TextView +import androidx.activity.result.contract.ActivityResultContracts import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import com.futo.platformplayer.R +import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.views.buttons.BigButton @@ -23,9 +27,12 @@ class LibraryFragment : MainFragment() { private var view: FragView? = null; + private var allowedMusic = false; + private var allowedVideo = false; + override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): android.view.View { - val newView = FragView(this); + val newView = FragView(this, allowedMusic, allowedVideo); view = newView; return newView; } @@ -33,6 +40,9 @@ class LibraryFragment : MainFragment() { override fun onShownWithView(parameter: Any?, isBack: Boolean) { super.onShownWithView(parameter, isBack); view?.onShown(); + + requestPermissionMusic(); + requestPermissionVideo(); } override fun onDestroyMainView() { @@ -40,6 +50,63 @@ class LibraryFragment : MainFragment() { super.onDestroyMainView(); } + fun setPermissionResultAudio(access: Boolean) { + allowedMusic = access; + view?.setMusicPermissions(access); + } + fun setPermissionResultVideo(access: Boolean) { + allowedVideo = access; + view?.setVideoPermissions(access); + } + + fun requestPermissionMusic() { + when { + ContextCompat.checkSelfPermission(requireContext(), android.Manifest.permission.READ_MEDIA_AUDIO) == PackageManager.PERMISSION_GRANTED -> { + setPermissionResultAudio(true); + } + ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), android.Manifest.permission.READ_MEDIA_AUDIO) -> { + UIDialogs.showDialog(requireContext(), R.drawable.ic_library, + "Music permissions", "We require permissions to see your on-device music, denying this will hide the option to see local music.", null, 1, + UIDialogs.Action("Ok", { + permissionReqAudio.launch(android.Manifest.permission.READ_MEDIA_AUDIO); + }, UIDialogs.ActionStyle.PRIMARY), + UIDialogs.Action("Cancel", { + + }, UIDialogs.ActionStyle.NONE)); + } + else -> { + permissionReqAudio.launch(android.Manifest.permission.READ_MEDIA_AUDIO); + } + } + } + fun requestPermissionVideo() { + when { + ContextCompat.checkSelfPermission(requireContext(), android.Manifest.permission.READ_MEDIA_VIDEO) == PackageManager.PERMISSION_GRANTED -> { + setPermissionResultVideo(true); + } + ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), android.Manifest.permission.READ_MEDIA_VIDEO) -> { + UIDialogs.showDialog(requireContext(), R.drawable.ic_library, false, + "Videos permissions", "We require permissions to see your on-device videos, denying this will hide the option to see local videos.", null, 1, + UIDialogs.Action("Ok", { + permissionReqVideo.launch(android.Manifest.permission.READ_MEDIA_VIDEO); + }, UIDialogs.ActionStyle.PRIMARY), + UIDialogs.Action("Cancel", { + + }, UIDialogs.ActionStyle.NONE)); + } + else -> { + permissionReqVideo.launch(android.Manifest.permission.READ_MEDIA_VIDEO); + } + } + } + + val permissionReqAudio = registerForActivityResult(ActivityResultContracts.RequestPermission(), { isGranted -> + setPermissionResultAudio(isGranted); + }); + val permissionReqVideo = registerForActivityResult(ActivityResultContracts.RequestPermission(), { isGranted -> + setPermissionResultVideo(isGranted); + }); + companion object { fun newInstance() = LibraryFragment().apply {} } @@ -47,30 +114,76 @@ class LibraryFragment : MainFragment() { class FragView: ConstraintLayout { val fragment: LibraryFragment; - constructor(fragment: LibraryFragment, attrs : AttributeSet? = null) : super(fragment.requireContext(), attrs) { + + var buttonArtists: BigButton; + var buttonAlbums: BigButton; + var buttonVideos: BigButton; + var buttonPlaylists: BigButton; + var buttonFiles: BigButton; + + var metaInfo: TextView; + + var allowMusic: Boolean = false; + var allowVideo: Boolean = false; + + constructor(fragment: LibraryFragment, allowMusic: Boolean?, allowVideo: Boolean?) : super(fragment.requireContext()) { inflate(context, R.layout.fragview_library, this); this.fragment = fragment; - val buttonArtists = findViewById(R.id.button_artists); - val buttonAlbums = findViewById(R.id.button_albums); - val buttonVideos = findViewById(R.id.button_videos); - val buttonPlaylists = findViewById(R.id.button_playlists); - val buttonFiles = findViewById(R.id.button_files); + buttonArtists = findViewById(R.id.button_artists); + buttonAlbums = findViewById(R.id.button_albums); + buttonVideos = findViewById(R.id.button_videos); + buttonPlaylists = findViewById(R.id.button_playlists); + buttonFiles = findViewById(R.id.button_files); + metaInfo = findViewById(R.id.meta_info); + + this.allowMusic = allowMusic ?: false; + this.allowVideo = allowVideo ?: false; buttonArtists.onClick.subscribe { - fragment.navigate(); + if(this.allowMusic) + fragment.navigate(); + else + fragment.requestPermissionMusic(); } buttonAlbums.onClick.subscribe { - fragment.navigate(); + if(this.allowMusic) + fragment.navigate(); + else + fragment.requestPermissionMusic(); } buttonVideos.onClick.subscribe { - fragment.navigate(); + if(this.allowVideo) + fragment.navigate(); + else + fragment.requestPermissionVideo(); } buttonPlaylists.onClick.subscribe { fragment.navigate(); } buttonFiles.onClick.subscribe { - + UIDialogs.appToast("This is gonna require a bit more work.."); } + buttonFiles.setButtonEnabled(false); + setMusicPermissions(allowMusic ?: false); + setVideoPermissions(allowVideo ?: false); + } + + fun setMusicPermissions(access: Boolean) { + allowMusic = access; + buttonAlbums.setButtonEnabled(access); + buttonArtists.setButtonEnabled(access); + metaInfo.text = listOf( + if(!allowMusic) "You did not give access to local music, so these options are disabled" else null, + if(!allowVideo) "You did not give access to local videos, so these options are disabled" else null + ).filterNotNull().joinToString("\n"); + } + fun setVideoPermissions(access: Boolean) { + allowVideo = access; + buttonVideos.setButtonEnabled(access); + metaInfo.text = listOf( + if(!allowMusic) "You did not give access to local music, so these options are disabled" else null, + if(!allowVideo) "You did not give access to local videos, so these options are disabled" else null + ).filterNotNull().joinToString("\n"); } fun onShown() { diff --git a/app/src/main/java/com/futo/platformplayer/states/StateLibrary.kt b/app/src/main/java/com/futo/platformplayer/states/StateLibrary.kt index 4c8cb04b..4070cc06 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateLibrary.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateLibrary.kt @@ -271,7 +271,7 @@ class Artist { } fun getAlbums(): List { - return listOf(); + return Album.getArtistAlbums(id.toLongOrNull() ?: return listOf()); } fun getAudioTracks(): IPager { @@ -438,5 +438,22 @@ class Album { } return list; } + fun getArtistAlbums(artistId: Long): List { + val resolver = StateApp.instance.contextOrNull?.contentResolver; + if(resolver == null) { + Logger.w(TAG, "Album contentResolver not found"); + return listOf(); + } + val cursor = resolver?.query( + MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, PROJECTION, "${MediaStore.Audio.Media.ARTIST_ID} = ?", arrayOf(artistId.toString()), + MediaStore.Audio.Albums.ALBUM + " ASC") ?: return listOf(); + cursor.moveToFirst(); + val list = mutableListOf() + while(!cursor.isAfterLast) { + list.add(fromCursor(cursor)); + cursor.moveToNext(); + } + return list; + } } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragview_library.xml b/app/src/main/res/layout/fragview_library.xml index 1153b133..3c04a074 100644 --- a/app/src/main/res/layout/fragview_library.xml +++ b/app/src/main/res/layout/fragview_library.xml @@ -65,6 +65,22 @@ android:layout_marginLeft="10dp" android:layout_marginRight="10dp" android:layout_marginBottom="8dp" /> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_artist.xml b/app/src/main/res/layout/list_artist.xml new file mode 100644 index 00000000..270cab96 --- /dev/null +++ b/app/src/main/res/layout/list_artist.xml @@ -0,0 +1,58 @@ + + + + + + + + + \ No newline at end of file