diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index 6275a49f1..201a6a761 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -30,7 +30,8 @@ import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.databinding.FragmentDetailsBinding import org.koitharu.kotatsu.details.ui.model.ChapterListItem -import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingInfoBottomSheet +import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration +import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter import org.koitharu.kotatsu.history.domain.PROGRESS_NONE import org.koitharu.kotatsu.image.ui.ImageActivity import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner @@ -69,7 +70,6 @@ class DetailsFragment : super.onViewCreated(view, savedInstanceState) binding.textViewAuthor.setOnClickListener(this) binding.imageViewCover.setOnClickListener(this) - binding.scrobblingLayout.root.setOnClickListener(this) binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance() binding.chipsTags.onChipClickListener = this viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated) @@ -203,35 +203,22 @@ class DetailsFragment : } } - private fun onScrobblingInfoChanged(scrobbling: ScrobblingInfo?) { - with(binding.scrobblingLayout) { - root.isVisible = scrobbling != null - if (scrobbling == null) { - CoilUtils.dispose(imageViewCover) - return - } - imageViewCover.newImageRequest(scrobbling.coverUrl)?.run { - placeholder(R.drawable.ic_placeholder) - fallback(R.drawable.ic_placeholder) - error(R.drawable.ic_error_placeholder) - lifecycle(viewLifecycleOwner) - enqueueWith(coil) - } - textViewTitle.text = scrobbling.title - textViewTitle.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, scrobbling.scrobbler.iconResId, 0) - ratingBar.rating = scrobbling.rating * ratingBar.numStars - textViewStatus.text = scrobbling.status?.let { - resources.getStringArray(R.array.scrobbling_statuses).getOrNull(it.ordinal) - } + private fun onScrobblingInfoChanged(scrobblings: List) { + var adapter = binding.recyclerViewScrobbling.adapter as? ScrollingInfoAdapter + binding.recyclerViewScrobbling.isGone = scrobblings.isEmpty() + if (adapter != null) { + adapter.items = scrobblings + } else { + adapter = ScrollingInfoAdapter(viewLifecycleOwner, coil, childFragmentManager) + adapter.items = scrobblings + binding.recyclerViewScrobbling.adapter = adapter + binding.recyclerViewScrobbling.addItemDecoration(ScrobblingItemDecoration()) } } override fun onClick(v: View) { val manga = viewModel.manga.value ?: return when (v.id) { - R.id.scrobbling_layout -> { - ScrobblingInfoBottomSheet.show(childFragmentManager) - } R.id.textView_author -> { startActivity( SearchActivity.newIntent( diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index e83fd1ae7..ab9ffd418 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -38,6 +38,7 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.scrobbling.domain.Scrobbler +import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.SingleLiveEvent @@ -54,13 +55,11 @@ class DetailsViewModel @AssistedInject constructor( mangaDataRepository: MangaDataRepository, private val bookmarksRepository: BookmarksRepository, private val settings: AppSettings, - scrobblers: Set<@JvmSuppressWildcards Scrobbler>, + private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>, private val imageGetter: Html.ImageGetter, mangaRepositoryFactory: MangaRepository.Factory, ) : BaseViewModel() { - private val scrobbler = scrobblers.first() // TODO support multiple scrobblers - private val delegate = MangaDetailsDelegate( intent = intent, settings = settings, @@ -121,10 +120,13 @@ class DetailsViewModel @AssistedInject constructor( val onMangaRemoved = SingleLiveEvent() val isScrobblingAvailable: Boolean - get() = scrobbler.isAvailable + get() = scrobblers.any { it.isAvailable } - val scrobblingInfo = scrobbler.observeScrobblingInfo(delegate.mangaId) - .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, null) + val scrobblingInfo: LiveData> = combine( + scrobblers.map { it.observeScrobblingInfo(delegate.mangaId) }, + ) { scrobblingInfo -> + scrobblingInfo.filterNotNull() + }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) val branches: LiveData> = delegate.manga.map { val chapters = it?.chapters ?: return@map emptyList() @@ -238,21 +240,27 @@ class DetailsViewModel @AssistedInject constructor( } fun updateScrobbling(rating: Float, status: ScrobblingStatus?) { - launchJob(Dispatchers.Default) { - scrobbler.updateScrobblingInfo( - mangaId = delegate.mangaId, - rating = rating, - status = status, - comment = null, - ) + for (scrobbler in scrobblers) { + if (!scrobbler.isAvailable) continue + launchJob(Dispatchers.Default) { + scrobbler.updateScrobblingInfo( + mangaId = delegate.mangaId, + rating = rating, + status = status, + comment = null, + ) + } } } fun unregisterScrobbling() { - launchJob(Dispatchers.Default) { - scrobbler.unregisterScrobbling( - mangaId = delegate.mangaId, - ) + for (scrobbler in scrobblers) { + if (!scrobbler.isAvailable) continue + launchJob(Dispatchers.Default) { + scrobbler.unregisterScrobbling( + mangaId = delegate.mangaId, + ) + } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoAD.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoAD.kt new file mode 100644 index 000000000..7c3af4dc8 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoAD.kt @@ -0,0 +1,44 @@ +package org.koitharu.kotatsu.details.ui.scrobbling + +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.LifecycleOwner +import coil.ImageLoader +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.databinding.ItemScrobblingInfoBinding +import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo +import org.koitharu.kotatsu.utils.ext.disposeImageRequest +import org.koitharu.kotatsu.utils.ext.enqueueWith +import org.koitharu.kotatsu.utils.ext.newImageRequest + +fun scrobblingInfoAD( + lifecycleOwner: LifecycleOwner, + coil: ImageLoader, + fragmentManager: FragmentManager, +) = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemScrobblingInfoBinding.inflate(layoutInflater, parent, false) }, +) { + binding.root.setOnClickListener { + ScrobblingInfoBottomSheet.show(fragmentManager, bindingAdapterPosition) + } + + bind { + binding.imageViewCover.newImageRequest(item.coverUrl)?.run { + placeholder(R.drawable.ic_placeholder) + fallback(R.drawable.ic_placeholder) + error(R.drawable.ic_error_placeholder) + lifecycle(lifecycleOwner) + enqueueWith(coil) + } + binding.textViewTitle.text = item.title + binding.textViewTitle.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, item.scrobbler.iconResId, 0) + binding.ratingBar.rating = item.rating * binding.ratingBar.numStars + binding.textViewStatus.text = item.status?.let { + context.resources.getStringArray(R.array.scrobbling_statuses).getOrNull(it.ordinal) + } + } + + onViewRecycled { + binding.imageViewCover.disposeImageRequest() + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoBottomSheet.kt index 0ec3b545a..f5f8510e5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoBottomSheet.kt @@ -26,10 +26,7 @@ import org.koitharu.kotatsu.image.ui.ImageActivity import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus import org.koitharu.kotatsu.scrobbling.ui.selector.ScrobblingSelectorBottomSheet -import org.koitharu.kotatsu.utils.ext.crossfade -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf +import org.koitharu.kotatsu.utils.ext.* @AndroidEntryPoint class ScrobblingInfoBottomSheet : @@ -40,11 +37,17 @@ class ScrobblingInfoBottomSheet : PopupMenu.OnMenuItemClickListener { private val viewModel by activityViewModels() + private var scrobblerIndex: Int = -1 @Inject lateinit var coil: ImageLoader private var menu: PopupMenu? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + scrobblerIndex = requireArguments().getInt(ARG_INDEX, scrobblerIndex) + } + override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetScrobblingBinding { return SheetScrobblingBinding.inflate(inflater, container, false) } @@ -95,14 +98,15 @@ class ScrobblingInfoBottomSheet : when (v.id) { R.id.button_menu -> menu?.show() R.id.imageView_cover -> { - val coverUrl = viewModel.scrobblingInfo.value?.coverUrl ?: return + val coverUrl = viewModel.scrobblingInfo.value?.getOrNull(scrobblerIndex)?.coverUrl ?: return val options = scaleUpActivityOptionsOf(v) startActivity(ImageActivity.newIntent(v.context, coverUrl), options.toBundle()) } } } - private fun onScrobblingInfoChanged(scrobbling: ScrobblingInfo?) { + private fun onScrobblingInfoChanged(scrobblings: List) { + val scrobbling = scrobblings.getOrNull(scrobblerIndex) if (scrobbling == null) { dismissAllowingStateLoss() return @@ -122,17 +126,10 @@ class ScrobblingInfoBottomSheet : .enqueueWith(coil) } - companion object { - - private const val TAG = "ScrobblingInfoBottomSheet" - - fun show(fm: FragmentManager) = ScrobblingInfoBottomSheet().show(fm, TAG) - } - override fun onMenuItemClick(item: MenuItem): Boolean { when (item.itemId) { R.id.action_browser -> { - val url = viewModel.scrobblingInfo.value?.externalUrl ?: return false + val url = viewModel.scrobblingInfo.value?.getOrNull(scrobblerIndex)?.externalUrl ?: return false val intent = Intent(Intent.ACTION_VIEW, url.toUri()) startActivity( Intent.createChooser(intent, getString(R.string.open_in_browser)), @@ -150,4 +147,14 @@ class ScrobblingInfoBottomSheet : } return true } + + companion object { + + private const val TAG = "ScrobblingInfoBottomSheet" + private const val ARG_INDEX = "index" + + fun show(fm: FragmentManager, index: Int) = ScrobblingInfoBottomSheet().withArgs(1) { + putInt(ARG_INDEX, index) + }.show(fm, TAG) + } } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingItemDecoration.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingItemDecoration.kt new file mode 100644 index 000000000..7b9d86300 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingItemDecoration.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.details.ui.scrobbling + +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import org.koitharu.kotatsu.R + +class ScrobblingItemDecoration() : RecyclerView.ItemDecoration() { + + private var spacing: Int = -1 + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + if (spacing == -1) { + spacing = parent.context.resources.getDimensionPixelOffset(R.dimen.scrobbling_list_spacing) + } + outRect.set(0, spacing, 0, 0) + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrollingInfoAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrollingInfoAdapter.kt new file mode 100644 index 000000000..9336b0e98 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrollingInfoAdapter.kt @@ -0,0 +1,34 @@ +package org.koitharu.kotatsu.details.ui.scrobbling + +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.DiffUtil +import coil.ImageLoader +import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter +import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo + +class ScrollingInfoAdapter( + lifecycleOwner: LifecycleOwner, + coil: ImageLoader, + fragmentManager: FragmentManager, +) : AsyncListDifferDelegationAdapter(DiffCallback()) { + + init { + delegatesManager.addDelegate(scrobblingInfoAD(lifecycleOwner, coil, fragmentManager)) + } + + private class DiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: ScrobblingInfo, newItem: ScrobblingInfo): Boolean { + return oldItem.scrobbler == newItem.scrobbler + } + + override fun areContentsTheSame(oldItem: ScrobblingInfo, newItem: ScrobblingInfo): Boolean { + return oldItem == newItem + } + + override fun getChangePayload(oldItem: ScrobblingInfo, newItem: ScrobblingInfo): Any? { + return Unit + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt index e278ae0ca..06045eae9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt @@ -4,8 +4,11 @@ import android.app.Dialog import android.content.DialogInterface import android.os.Bundle import android.view.* +import android.widget.AdapterView +import android.widget.ArrayAdapter import android.widget.Toast import androidx.appcompat.widget.SearchView +import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager import coil.ImageLoader import dagger.hilt.android.AndroidEntryPoint @@ -33,7 +36,8 @@ class ScrobblingSelectorBottomSheet : View.OnClickListener, MenuItem.OnActionExpandListener, SearchView.OnQueryTextListener, - DialogInterface.OnKeyListener { + DialogInterface.OnKeyListener, + AdapterView.OnItemSelectedListener { @Inject lateinit var viewModelFactory: ScrobblingSelectorViewModel.Factory @@ -68,6 +72,7 @@ class ScrobblingSelectorBottomSheet : } binding.buttonDone.setOnClickListener(this) initOptionsMenu() + initSpinner() viewModel.content.observe(viewLifecycleOwner) { listAdapter.items = it } viewModel.selectedItemId.observe(viewLifecycleOwner) { @@ -133,6 +138,12 @@ class ScrobblingSelectorBottomSheet : return false } + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + viewModel.setScrobblerIndex(position) + } + + override fun onNothingSelected(parent: AdapterView<*>?) = Unit + private fun onError(e: Throwable) { Toast.makeText(requireContext(), e.getDisplayMessage(resources), Toast.LENGTH_LONG).show() if (viewModel.isEmpty) { @@ -150,6 +161,21 @@ class ScrobblingSelectorBottomSheet : searchView.queryHint = searchMenuItem.title } + private fun initSpinner() { + val entries = viewModel.availableScrobblers + if (entries.size <= 1) { + binding.spinnerScrobblers.isVisible = false + return + } + val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, entries) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.spinnerScrobblers.adapter = adapter + viewModel.selectedScrobblerIndex.observe(viewLifecycleOwner) { + binding.spinnerScrobblers.setSelection(it) + } + binding.spinnerScrobblers.onItemSelectedListener = this + } + companion object { private const val TAG = "ScrobblingSelectorBottomSheet" diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorViewModel.kt index d6b111b3d..a9509e2b7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorViewModel.kt @@ -21,21 +21,28 @@ import org.koitharu.kotatsu.scrobbling.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct +import org.koitharu.kotatsu.utils.ext.requireValue class ScrobblingSelectorViewModel @AssistedInject constructor( @Assisted val manga: Manga, scrobblers: Set<@JvmSuppressWildcards Scrobbler>, ) : BaseViewModel() { - private val scrobbler = scrobblers.first() // TODO support multiple scrobblers + val availableScrobblers = scrobblers.filter { it.isAvailable } - private val shikiMangaList = MutableStateFlow?>(null) + val selectedScrobblerIndex = MutableLiveData(0) + + private val scrobblerMangaList = MutableStateFlow?>(null) private val hasNextPage = MutableStateFlow(false) private var loadingJob: Job? = null private var doneJob: Job? = null + private var initJob: Job? = null + + private val currentScrobbler: Scrobbler + get() = availableScrobblers[selectedScrobblerIndex.requireValue()] val content: LiveData> = combine( - shikiMangaList.filterNotNull(), + scrobblerMangaList.filterNotNull(), hasNextPage, ) { list, isHasNextPage -> when { @@ -50,19 +57,10 @@ class ScrobblingSelectorViewModel @AssistedInject constructor( val onClose = SingleLiveEvent() val isEmpty: Boolean - get() = shikiMangaList.value.isNullOrEmpty() + get() = scrobblerMangaList.value.isNullOrEmpty() init { - launchJob(Dispatchers.Default) { - try { - val info = scrobbler.getScrobblingInfoOrNull(manga.id) - if (info != null) { - selectedItemId.postValue(info.targetId) - } - } finally { - loadList(append = false) - } - } + initialize() } fun search(query: String) { @@ -79,12 +77,12 @@ class ScrobblingSelectorViewModel @AssistedInject constructor( return } loadingJob = launchLoadingJob(Dispatchers.Default) { - val offset = if (append) shikiMangaList.value?.size ?: 0 else 0 - val list = scrobbler.findManga(checkNotNull(searchQuery.value), offset) + val offset = if (append) scrobblerMangaList.value?.size ?: 0 else 0 + val list = currentScrobbler.findManga(checkNotNull(searchQuery.value), offset) if (!append) { - shikiMangaList.value = list + scrobblerMangaList.value = list } else if (list.isNotEmpty()) { - shikiMangaList.value = shikiMangaList.value?.plus(list) ?: list + scrobblerMangaList.value = scrobblerMangaList.value?.plus(list) ?: list } hasNextPage.value = list.isNotEmpty() } @@ -99,11 +97,34 @@ class ScrobblingSelectorViewModel @AssistedInject constructor( onClose.call(Unit) } doneJob = launchJob(Dispatchers.Default) { - scrobbler.linkManga(manga.id, targetId) + currentScrobbler.linkManga(manga.id, targetId) onClose.postCall(Unit) } } + fun setScrobblerIndex(index: Int) { + if (index == selectedScrobblerIndex.value || index !in availableScrobblers.indices) return + selectedScrobblerIndex.value = index + initialize() + } + + private fun initialize() { + initJob?.cancel() + loadingJob?.cancel() + hasNextPage.value = false + scrobblerMangaList.value = null + initJob = launchJob(Dispatchers.Default) { + try { + val info = currentScrobbler.getScrobblingInfoOrNull(manga.id) + if (info != null) { + selectedItemId.postValue(info.targetId) + } + } finally { + loadList(append = false) + } + } + } + @AssistedFactory interface Factory { diff --git a/app/src/main/res/layout-w600dp/fragment_details.xml b/app/src/main/res/layout-w600dp/fragment_details.xml index 29aaefc20..a9f6926dc 100644 --- a/app/src/main/res/layout-w600dp/fragment_details.xml +++ b/app/src/main/res/layout-w600dp/fragment_details.xml @@ -157,19 +157,24 @@ app:layout_constraintTop_toBottomOf="@id/textView_bookmarks" tools:listitem="@layout/item_bookmark" /> - diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml index 82c663198..90db85516 100644 --- a/app/src/main/res/layout/fragment_details.xml +++ b/app/src/main/res/layout/fragment_details.xml @@ -169,19 +169,24 @@ app:layout_constraintTop_toBottomOf="@id/textView_bookmarks" tools:listitem="@layout/item_bookmark" /> - diff --git a/app/src/main/res/layout/layout_scrobbling_info.xml b/app/src/main/res/layout/item_scrobbling_info.xml similarity index 96% rename from app/src/main/res/layout/layout_scrobbling_info.xml rename to app/src/main/res/layout/item_scrobbling_info.xml index 739c73c84..a3d589a9e 100644 --- a/app/src/main/res/layout/layout_scrobbling_info.xml +++ b/app/src/main/res/layout/item_scrobbling_info.xml @@ -3,10 +3,9 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" - android:id="@+id/scrobbling_layout" style="@style/Widget.Material3.CardView.Filled" android:layout_width="match_parent" - android:layout_height="match_parent" + android:layout_height="wrap_content" app:contentPadding="8dp"> + + 84dp 4dp 10dp + 12dp 124dp 4dp