Improve scrobbling ui

This commit is contained in:
Koitharu
2023-02-01 20:21:20 +02:00
parent 205a2e10a5
commit fd26de7619
15 changed files with 166 additions and 97 deletions

1
.gitignore vendored
View File

@@ -15,6 +15,7 @@
/.idea/deploymentTargetDropDown.xml /.idea/deploymentTargetDropDown.xml
/.idea/androidTestResultsUserPreferences.xml /.idea/androidTestResultsUserPreferences.xml
/.idea/render.experimental.xml /.idea/render.experimental.xml
/.idea/inspectionProfiles/
.DS_Store .DS_Store
/build /build
/captures /captures

View File

@@ -1,17 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="BooleanLiteralArgument" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="Destructure" enabled="true" level="INFO" enabled_by_default="true" />
<inspection_tool class="FillClass" enabled="true" level="INFORMATION" enabled_by_default="true">
<option name="withoutDefaultValues" value="true" />
</inspection_tool>
<inspection_tool class="KeySetIterationMayUseEntrySet" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="KotlinFunctionArgumentsHelper" enabled="true" level="INFORMATION" enabled_by_default="true">
<option name="withoutDefaultValues" value="true" />
</inspection_tool>
<inspection_tool class="ReplaceCollectionCountWithSize" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="TrailingComma" enabled="true" level="INFORMATION" enabled_by_default="true" />
<inspection_tool class="ZeroLengthArrayInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

View File

@@ -9,13 +9,13 @@ import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams import android.view.ViewGroup.LayoutParams
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.android.material.R as materialR
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.dialog.AppBottomSheetDialog import org.koitharu.kotatsu.base.ui.dialog.AppBottomSheetDialog
import org.koitharu.kotatsu.utils.ext.displayCompat import org.koitharu.kotatsu.utils.ext.displayCompat
import com.google.android.material.R as materialR
abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() { abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
@@ -27,6 +27,9 @@ abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
protected val behavior: BottomSheetBehavior<*>? protected val behavior: BottomSheetBehavior<*>?
get() = (dialog as? BottomSheetDialog)?.behavior get() = (dialog as? BottomSheetDialog)?.behavior
val isExpanded: Boolean
get() = behavior?.state == BottomSheetBehavior.STATE_EXPANDED
final override fun onCreateView( final override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,

View File

@@ -42,7 +42,7 @@ class DetailsMenuProvider(
menu.findItem(R.id.action_delete).isVisible = manga?.source == MangaSource.LOCAL menu.findItem(R.id.action_delete).isVisible = manga?.source == MangaSource.LOCAL
menu.findItem(R.id.action_browser).isVisible = manga?.source != MangaSource.LOCAL menu.findItem(R.id.action_browser).isVisible = manga?.source != MangaSource.LOCAL
menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity) menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity)
menu.findItem(R.id.action_shiki_track).isVisible = viewModel.isScrobblingAvailable menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable
menu.findItem(R.id.action_favourite).setIcon( menu.findItem(R.id.action_favourite).setIcon(
if (viewModel.favouriteCategories.value == true) R.drawable.ic_heart else R.drawable.ic_heart_outline, if (viewModel.favouriteCategories.value == true) R.drawable.ic_heart else R.drawable.ic_heart_outline,
) )
@@ -60,11 +60,13 @@ class DetailsMenuProvider(
} }
} }
} }
R.id.action_favourite -> { R.id.action_favourite -> {
viewModel.manga.value?.let { viewModel.manga.value?.let {
FavouriteCategoriesBottomSheet.show(activity.supportFragmentManager, it) FavouriteCategoriesBottomSheet.show(activity.supportFragmentManager, it)
} }
} }
R.id.action_delete -> { R.id.action_delete -> {
val title = viewModel.manga.value?.title.orEmpty() val title = viewModel.manga.value?.title.orEmpty()
MaterialAlertDialogBuilder(activity) MaterialAlertDialogBuilder(activity)
@@ -76,6 +78,7 @@ class DetailsMenuProvider(
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show() .show()
} }
R.id.action_save -> { R.id.action_save -> {
viewModel.manga.value?.let { viewModel.manga.value?.let {
val chaptersCount = it.chapters?.size ?: 0 val chaptersCount = it.chapters?.size ?: 0
@@ -87,21 +90,25 @@ class DetailsMenuProvider(
} }
} }
} }
R.id.action_browser -> { R.id.action_browser -> {
viewModel.manga.value?.let { viewModel.manga.value?.let {
activity.startActivity(BrowserActivity.newIntent(activity, it.publicUrl, it.title)) activity.startActivity(BrowserActivity.newIntent(activity, it.publicUrl, it.title))
} }
} }
R.id.action_related -> { R.id.action_related -> {
viewModel.manga.value?.let { viewModel.manga.value?.let {
activity.startActivity(MultiSearchActivity.newIntent(activity, it.title)) activity.startActivity(MultiSearchActivity.newIntent(activity, it.title))
} }
} }
R.id.action_shiki_track -> {
R.id.action_scrobbling -> {
viewModel.manga.value?.let { viewModel.manga.value?.let {
ScrobblingSelectorBottomSheet.show(activity.supportFragmentManager, it) ScrobblingSelectorBottomSheet.show(activity.supportFragmentManager, it, null)
} }
} }
R.id.action_shortcut -> { R.id.action_shortcut -> {
viewModel.manga.value?.let { viewModel.manga.value?.let {
activity.lifecycleScope.launch { activity.lifecycleScope.launch {
@@ -112,6 +119,7 @@ class DetailsMenuProvider(
} }
} }
} }
else -> return false else -> return false
} }
return true return true

View File

@@ -256,29 +256,24 @@ class DetailsViewModel @AssistedInject constructor(
} }
} }
fun updateScrobbling(rating: Float, status: ScrobblingStatus?) { fun updateScrobbling(index: Int, rating: Float, status: ScrobblingStatus?) {
for (info in scrobblingInfo.value ?: return) { val scrobbler = getScrobbler(index) ?: return
val scrobbler = scrobblers.first { it.scrobblerService == info.scrobbler } launchJob(Dispatchers.Default) {
if (!scrobbler.isAvailable) continue scrobbler.updateScrobblingInfo(
launchJob(Dispatchers.Default) { mangaId = delegate.mangaId,
scrobbler.updateScrobblingInfo( rating = rating,
mangaId = delegate.mangaId, status = status,
rating = rating, comment = null,
status = status, )
comment = null,
)
}
} }
} }
fun unregisterScrobbling() { fun unregisterScrobbling(index: Int) {
for (scrobbler in scrobblers) { val scrobbler = getScrobbler(index) ?: return
if (!scrobbler.isAvailable) continue launchJob(Dispatchers.Default) {
launchJob(Dispatchers.Default) { scrobbler.unregisterScrobbling(
scrobbler.unregisterScrobbling( mangaId = delegate.mangaId,
mangaId = delegate.mangaId, )
)
}
} }
} }
@@ -315,6 +310,19 @@ class DetailsViewModel @AssistedInject constructor(
return spannable.trim() return spannable.trim()
} }
private fun getScrobbler(index: Int): Scrobbler? {
val info = scrobblingInfo.value?.getOrNull(index)
val scrobbler = if (info != null) {
scrobblers.find { it.scrobblerService == info.scrobbler && it.isAvailable }
} else {
null
}
if (scrobbler == null) {
errorEvent.call(IllegalStateException("Scrobbler [$index] is not available"))
}
return scrobbler
}
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {

View File

@@ -15,9 +15,7 @@ import androidx.core.net.toUri
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.databinding.SheetScrobblingBinding import org.koitharu.kotatsu.databinding.SheetScrobblingBinding
@@ -26,7 +24,12 @@ import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus
import org.koitharu.kotatsu.scrobbling.ui.selector.ScrobblingSelectorBottomSheet import org.koitharu.kotatsu.scrobbling.ui.selector.ScrobblingSelectorBottomSheet
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.utils.ext.withArgs
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ScrobblingInfoBottomSheet : class ScrobblingInfoBottomSheet :
@@ -41,6 +44,7 @@ class ScrobblingInfoBottomSheet :
@Inject @Inject
lateinit var coil: ImageLoader lateinit var coil: ImageLoader
private var menu: PopupMenu? = null private var menu: PopupMenu? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -78,6 +82,7 @@ class ScrobblingInfoBottomSheet :
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
viewModel.updateScrobbling( viewModel.updateScrobbling(
index = scrobblerIndex,
rating = binding.ratingBar.rating / binding.ratingBar.numStars, rating = binding.ratingBar.rating / binding.ratingBar.numStars,
status = enumValues<ScrobblingStatus>().getOrNull(position), status = enumValues<ScrobblingStatus>().getOrNull(position),
) )
@@ -88,6 +93,7 @@ class ScrobblingInfoBottomSheet :
override fun onRatingChanged(ratingBar: RatingBar, rating: Float, fromUser: Boolean) { override fun onRatingChanged(ratingBar: RatingBar, rating: Float, fromUser: Boolean) {
if (fromUser) { if (fromUser) {
viewModel.updateScrobbling( viewModel.updateScrobbling(
index = scrobblerIndex,
rating = rating / ratingBar.numStars, rating = rating / ratingBar.numStars,
status = enumValues<ScrobblingStatus>().getOrNull(binding.spinnerStatus.selectedItemPosition), status = enumValues<ScrobblingStatus>().getOrNull(binding.spinnerStatus.selectedItemPosition),
) )
@@ -115,15 +121,15 @@ class ScrobblingInfoBottomSheet :
binding.ratingBar.rating = scrobbling.rating * binding.ratingBar.numStars binding.ratingBar.rating = scrobbling.rating * binding.ratingBar.numStars
binding.textViewDescription.text = scrobbling.description binding.textViewDescription.text = scrobbling.description
binding.spinnerStatus.setSelection(scrobbling.status?.ordinal ?: -1) binding.spinnerStatus.setSelection(scrobbling.status?.ordinal ?: -1)
ImageRequest.Builder(context ?: return) binding.imageViewLogo.contentDescription = getString(scrobbling.scrobbler.titleResId)
.target(binding.imageViewCover) binding.imageViewLogo.setImageResource(scrobbling.scrobbler.iconResId)
.data(scrobbling.coverUrl) binding.imageViewCover.newImageRequest(scrobbling.coverUrl)?.apply {
.crossfade(context) lifecycle(viewLifecycleOwner)
.lifecycle(viewLifecycleOwner) placeholder(R.drawable.ic_placeholder)
.placeholder(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder) error(R.drawable.ic_error_placeholder)
.error(R.drawable.ic_error_placeholder) enqueueWith(coil)
.enqueueWith(coil) }
} }
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {
@@ -135,13 +141,16 @@ class ScrobblingInfoBottomSheet :
Intent.createChooser(intent, getString(R.string.open_in_browser)), Intent.createChooser(intent, getString(R.string.open_in_browser)),
) )
} }
R.id.action_unregister -> { R.id.action_unregister -> {
viewModel.unregisterScrobbling() viewModel.unregisterScrobbling(scrobblerIndex)
dismiss() dismiss()
} }
R.id.action_edit -> { R.id.action_edit -> {
val manga = viewModel.manga.value ?: return false val manga = viewModel.manga.value ?: return false
ScrobblingSelectorBottomSheet.show(parentFragmentManager, manga) val scrobblerService = viewModel.scrobblingInfo.value?.getOrNull(scrobblerIndex)?.scrobbler
ScrobblingSelectorBottomSheet.show(parentFragmentManager, manga, scrobblerService)
dismiss() dismiss()
} }
} }

View File

@@ -8,13 +8,12 @@ import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import coil.ImageLoader import coil.ImageLoader
import com.google.android.material.tabs.TabLayout
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaIntent import org.koitharu.kotatsu.base.domain.MangaIntent
@@ -23,11 +22,14 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.databinding.SheetScrobblingSelectorBinding import org.koitharu.kotatsu.databinding.SheetScrobblingSelectorBinding
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ShikiMangaSelectionDecoration import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ShikiMangaSelectionDecoration
import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ShikimoriSelectorAdapter import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ShikimoriSelectorAdapter
import org.koitharu.kotatsu.utils.ext.assistedViewModels import org.koitharu.kotatsu.utils.ext.assistedViewModels
import org.koitharu.kotatsu.utils.ext.firstVisibleItemPosition
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.requireParcelable import org.koitharu.kotatsu.utils.ext.requireParcelable
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
@@ -42,7 +44,8 @@ class ScrobblingSelectorBottomSheet :
MenuItem.OnActionExpandListener, MenuItem.OnActionExpandListener,
SearchView.OnQueryTextListener, SearchView.OnQueryTextListener,
DialogInterface.OnKeyListener, DialogInterface.OnKeyListener,
AdapterView.OnItemSelectedListener { TabLayout.OnTabSelectedListener,
ListStateHolderListener {
@Inject @Inject
lateinit var viewModelFactory: ScrobblingSelectorViewModel.Factory lateinit var viewModelFactory: ScrobblingSelectorViewModel.Factory
@@ -68,7 +71,7 @@ class ScrobblingSelectorBottomSheet :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val listAdapter = ShikimoriSelectorAdapter(viewLifecycleOwner, coil, this) val listAdapter = ShikimoriSelectorAdapter(viewLifecycleOwner, coil, this, this)
val decoration = ShikiMangaSelectionDecoration(view.context) val decoration = ShikiMangaSelectionDecoration(view.context)
with(binding.recyclerView) { with(binding.recyclerView) {
adapter = listAdapter adapter = listAdapter
@@ -77,7 +80,7 @@ class ScrobblingSelectorBottomSheet :
} }
binding.buttonDone.setOnClickListener(this) binding.buttonDone.setOnClickListener(this)
initOptionsMenu() initOptionsMenu()
initSpinner() initTabs()
viewModel.content.observe(viewLifecycleOwner) { listAdapter.items = it } viewModel.content.observe(viewLifecycleOwner) { listAdapter.items = it }
viewModel.selectedItemId.observe(viewLifecycleOwner) { viewModel.selectedItemId.observe(viewLifecycleOwner) {
@@ -103,6 +106,12 @@ class ScrobblingSelectorBottomSheet :
viewModel.selectedItemId.value = item.id viewModel.selectedItemId.value = item.id
} }
override fun onRetryClick(error: Throwable) = Unit
override fun onEmptyActionClick() {
openSearch()
}
override fun onScrolledToEnd() { override fun onScrolledToEnd() {
viewModel.loadList(append = true) viewModel.loadList(append = true)
} }
@@ -143,11 +152,23 @@ class ScrobblingSelectorBottomSheet :
return false return false
} }
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onTabSelected(tab: TabLayout.Tab) {
viewModel.setScrobblerIndex(position) viewModel.setScrobblerIndex(tab.position)
} }
override fun onNothingSelected(parent: AdapterView<*>?) = Unit override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
override fun onTabReselected(tab: TabLayout.Tab?) {
if (!isExpanded) {
setExpanded(isExpanded = true, isLocked = behavior?.isDraggable == false)
}
binding.recyclerView.firstVisibleItemPosition = 0
}
private fun openSearch() {
val menuItem = binding.headerBar.menu.findItem(R.id.action_search) ?: return
menuItem.expandActionView()
}
private fun onError(e: Throwable) { private fun onError(e: Throwable) {
Toast.makeText(requireContext(), e.getDisplayMessage(resources), Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
@@ -166,32 +187,41 @@ class ScrobblingSelectorBottomSheet :
searchView.queryHint = searchMenuItem.title searchView.queryHint = searchMenuItem.title
} }
private fun initSpinner() { private fun initTabs() {
val entries = viewModel.availableScrobblers val entries = viewModel.availableScrobblers
val tabs = binding.tabs
if (entries.size <= 1) { if (entries.size <= 1) {
binding.spinnerScrobblers.isVisible = false tabs.isVisible = false
return return
} }
val adapter = ArrayAdapter( val selectedId = arguments?.getInt(ARG_SCROBBLER, -1) ?: -1
requireContext(), tabs.removeAllTabs()
android.R.layout.simple_spinner_item, tabs.clearOnTabSelectedListeners()
entries.map { getString(it.scrobblerService.titleResId) }, tabs.addOnTabSelectedListener(this)
) for (entry in entries) {
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) val tab = tabs.newTab()
binding.spinnerScrobblers.adapter = adapter tab.tag = entry.scrobblerService
viewModel.selectedScrobblerIndex.observe(viewLifecycleOwner) { tab.setIcon(entry.scrobblerService.iconResId)
binding.spinnerScrobblers.setSelection(it) tab.setText(entry.scrobblerService.titleResId)
tabs.addTab(tab)
if (entry.scrobblerService.id == selectedId) {
tab.select()
}
} }
binding.spinnerScrobblers.onItemSelectedListener = this tabs.isVisible = true
} }
companion object { companion object {
private const val TAG = "ScrobblingSelectorBottomSheet" private const val TAG = "ScrobblingSelectorBottomSheet"
private const val ARG_SCROBBLER = "scrobbler"
fun show(fm: FragmentManager, manga: Manga) = fun show(fm: FragmentManager, manga: Manga, scrobblerService: ScrobblerService?) =
ScrobblingSelectorBottomSheet().withArgs(1) { ScrobblingSelectorBottomSheet().withArgs(2) {
putParcelable(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = false)) putParcelable(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = false))
if (scrobblerService != null) {
putInt(ARG_SCROBBLER, scrobblerService.id)
}
}.show(fm, TAG) }.show(fm, TAG)
} }
} }

View File

@@ -12,7 +12,9 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyHint
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.LoadingState
@@ -46,7 +48,7 @@ class ScrobblingSelectorViewModel @AssistedInject constructor(
hasNextPage, hasNextPage,
) { list, isHasNextPage -> ) { list, isHasNextPage ->
when { when {
list.isEmpty() -> listOf() list.isEmpty() -> listOf(emptyResultsHint())
isHasNextPage -> list + LoadingFooter isHasNextPage -> list + LoadingFooter
else -> list else -> list
} }
@@ -125,6 +127,13 @@ class ScrobblingSelectorViewModel @AssistedInject constructor(
} }
} }
private fun emptyResultsHint() = EmptyHint(
icon = R.drawable.ic_empty_history,
textPrimary = R.string.nothing_found,
textSecondary = R.string.text_search_holder_secondary,
actionStringRes = R.string.search,
)
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {

View File

@@ -13,7 +13,7 @@ import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.textAndVisible import org.koitharu.kotatsu.utils.ext.textAndVisible
fun shikimoriMangaAD( fun scrobblingMangaAD(
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
coil: ImageLoader, coil: ImageLoader,
clickListener: OnListItemClickListener<ScrobblerManga>, clickListener: OnListItemClickListener<ScrobblerManga>,

View File

@@ -4,23 +4,27 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import kotlin.jvm.internal.Intrinsics
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.list.ui.adapter.emptyHintAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
import kotlin.jvm.internal.Intrinsics
class ShikimoriSelectorAdapter( class ShikimoriSelectorAdapter(
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
coil: ImageLoader, coil: ImageLoader,
clickListener: OnListItemClickListener<ScrobblerManga>, clickListener: OnListItemClickListener<ScrobblerManga>,
stateHolderListener: ListStateHolderListener,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) { ) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
init { init {
delegatesManager.addDelegate(loadingStateAD()) delegatesManager.addDelegate(loadingStateAD())
.addDelegate(shikimoriMangaAD(lifecycleOwner, coil, clickListener)) .addDelegate(scrobblingMangaAD(lifecycleOwner, coil, clickListener))
.addDelegate(loadingFooterAD()) .addDelegate(loadingFooterAD())
.addDelegate(emptyHintAD(stateHolderListener))
} }
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() { private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
@@ -37,4 +41,4 @@ class ShikimoriSelectorAdapter(
return Intrinsics.areEqual(oldItem, newItem) return Intrinsics.areEqual(oldItem, newItem)
} }
} }
} }

View File

@@ -217,7 +217,8 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
viewLifecycleScope.launch { viewLifecycleScope.launch {
pref.summary = withContext(Dispatchers.Default) { pref.summary = withContext(Dispatchers.Default) {
runCatching { runCatching {
repository.loadUser().nickname val user = repository.loadUser()
getString(R.string.logged_in_as, user.nickname)
}.getOrElse { }.getOrElse {
it.printStackTraceDebug() it.printStackTraceDebug()
it.getDisplayMessage(resources) it.getDisplayMessage(resources)

File diff suppressed because one or more lines are too long

View File

@@ -36,6 +36,17 @@
tools:background="@sample/covers[9]" tools:background="@sample/covers[9]"
tools:ignore="ContentDescription,UnusedAttribute" /> tools:ignore="ContentDescription,UnusedAttribute" />
<ImageView
android:id="@+id/imageView_logo"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="@dimen/card_indicator_offset"
app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
app:layout_constraintEnd_toEndOf="@id/imageView_cover"
app:tint="?colorControlLight"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_shikimori" />
<TextView <TextView
android:id="@+id/textView_title" android:id="@+id/textView_title"
android:layout_width="0dp" android:layout_width="0dp"

View File

@@ -24,11 +24,13 @@
</org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar> </org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar>
<Spinner <com.google.android.material.tabs.TabLayout
android:id="@+id/spinner_scrobblers" android:id="@+id/tabs"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:listitem="@android:layout/simple_spinner_item" /> android:visibility="gone"
app:tabGravity="start"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
@@ -36,7 +38,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipToPadding="false" android:clipToPadding="false"
android:padding="@dimen/grid_spacing" android:padding="@dimen/grid_spacing"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical" android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_manga_list" /> tools:listitem="@layout/item_manga_list" />

View File

@@ -32,7 +32,7 @@
app:showAsAction="never" /> app:showAsAction="never" />
<item <item
android:id="@+id/action_shiki_track" android:id="@+id/action_scrobbling"
android:orderInCategory="50" android:orderInCategory="50"
android:title="@string/tracking" android:title="@string/tracking"
app:showAsAction="never" /> app:showAsAction="never" />