Scrobblers config activity
@ -1,3 +1,6 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
interface ListModel
|
||||
interface ListModel {
|
||||
|
||||
override fun equals(other: Any?): Boolean
|
||||
}
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
object LoadingFooter : ListModel
|
||||
object LoadingFooter : ListModel {
|
||||
|
||||
override fun equals(other: Any?): Boolean = other === LoadingFooter
|
||||
}
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
object LoadingState : ListModel
|
||||
object LoadingState : ListModel {
|
||||
|
||||
override fun equals(other: Any?): Boolean = other === LoadingState
|
||||
}
|
||||
|
||||
@ -1,86 +0,0 @@
|
||||
package org.koitharu.kotatsu.scrobbling.anilist.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.CircleCropTransformation
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.utils.PreferenceIconTarget
|
||||
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AniListSettingsFragment : BasePreferenceFragment(R.string.anilist) {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: AniListSettingsViewModel.Factory
|
||||
|
||||
private val viewModel by assistedViewModels {
|
||||
viewModelFactory.create(arguments?.getString(ARG_AUTH_CODE))
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_anilist)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.user.observe(viewLifecycleOwner, this::onUserChanged)
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
KEY_USER -> openAuthorization()
|
||||
KEY_LOGOUT -> {
|
||||
viewModel.logout()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onUserChanged(user: ScrobblerUser?) {
|
||||
val pref = findPreference<Preference>(KEY_USER) ?: return
|
||||
pref.isSelectable = user == null
|
||||
pref.title = user?.nickname ?: getString(R.string.sign_in)
|
||||
ImageRequest.Builder(requireContext())
|
||||
.data(user?.avatar)
|
||||
.transformations(CircleCropTransformation())
|
||||
.target(PreferenceIconTarget(pref))
|
||||
.enqueueWith(coil)
|
||||
findPreference<Preference>(KEY_LOGOUT)?.isVisible = user != null
|
||||
}
|
||||
|
||||
private fun openAuthorization(): Boolean {
|
||||
return runCatching {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse(viewModel.authorizationUrl)
|
||||
startActivity(intent)
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_USER = "al_user"
|
||||
private const val KEY_LOGOUT = "al_logout"
|
||||
|
||||
private const val ARG_AUTH_CODE = "auth_code"
|
||||
|
||||
fun newInstance(authCode: String?) = AniListSettingsFragment().withArgs(1) {
|
||||
putString(ARG_AUTH_CODE, authCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
package org.koitharu.kotatsu.scrobbling.anilist.ui
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
|
||||
class AniListSettingsViewModel @AssistedInject constructor(
|
||||
private val repository: AniListRepository,
|
||||
@Assisted authCode: String?,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val authorizationUrl: String
|
||||
get() = repository.oauthUrl
|
||||
|
||||
val user = MutableLiveData<ScrobblerUser?>()
|
||||
|
||||
init {
|
||||
if (authCode != null) {
|
||||
authorize(authCode)
|
||||
} else {
|
||||
loadUser()
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
repository.logout()
|
||||
user.postValue(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadUser() = launchJob(Dispatchers.Default) {
|
||||
val userModel = if (repository.isAuthorized) {
|
||||
repository.cachedUser?.let(user::postValue)
|
||||
repository.loadUser()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
user.postValue(userModel)
|
||||
}
|
||||
|
||||
private fun authorize(code: String) = launchJob(Dispatchers.Default) {
|
||||
repository.authorize(code)
|
||||
user.postValue(repository.loadUser())
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
||||
fun create(authCode: String?): AniListSettingsViewModel
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
package org.koitharu.kotatsu.scrobbling.data
|
||||
package org.koitharu.kotatsu.scrobbling.common.data
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
|
||||
|
||||
interface ScrobblerRepository {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
package org.koitharu.kotatsu.scrobbling.data
|
||||
package org.koitharu.kotatsu.scrobbling.common.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import org.jsoup.internal.StringUtil.StringJoiner
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
|
||||
|
||||
private const val KEY_ACCESS_TOKEN = "access_token"
|
||||
private const val KEY_REFRESH_TOKEN = "refresh_token"
|
||||
@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.data
|
||||
package org.koitharu.kotatsu.scrobbling.common.data
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.domain.model
|
||||
package org.koitharu.kotatsu.scrobbling.common.domain.model
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.domain.model
|
||||
package org.koitharu.kotatsu.scrobbling.common.domain.model
|
||||
|
||||
class ScrobblerMangaInfo(
|
||||
val id: Long,
|
||||
@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.domain.model
|
||||
package org.koitharu.kotatsu.scrobbling.common.domain.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.domain.model
|
||||
package org.koitharu.kotatsu.scrobbling.common.domain.model
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.domain.model
|
||||
package org.koitharu.kotatsu.scrobbling.common.domain.model
|
||||
|
||||
class ScrobblerUser(
|
||||
val id: Long,
|
||||
@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.scrobbling.common.domain.model
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
enum class ScrobblingStatus : ListModel {
|
||||
|
||||
PLANNED,
|
||||
READING,
|
||||
RE_READING,
|
||||
COMPLETED,
|
||||
ON_HOLD,
|
||||
DROPPED,
|
||||
}
|
||||
@ -0,0 +1,188 @@
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.config
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import coil.ImageLoader
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.base.ui.list.decor.TypedSpacingItemDecoration
|
||||
import org.koitharu.kotatsu.databinding.ActivityScrobblerConfigBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.config.adapter.ScrobblingMangaAdapter
|
||||
import org.koitharu.kotatsu.tracker.ui.feed.adapter.FeedAdapter
|
||||
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.hideCompat
|
||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.utils.ext.showCompat
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ScrobblerConfigActivity : BaseActivity<ActivityScrobblerConfigBinding>(),
|
||||
OnListItemClickListener<ScrobblingInfo>, View.OnClickListener {
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ScrobblerConfigViewModel.Factory
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
private val viewModel: ScrobblerConfigViewModel by assistedViewModels {
|
||||
viewModelFactory.create(requireNotNull(getScrobblerService(intent)))
|
||||
}
|
||||
|
||||
private var paddingVertical = 0
|
||||
private var paddingHorizontal = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityScrobblerConfigBinding.inflate(layoutInflater))
|
||||
setTitle(viewModel.titleResId)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
val listAdapter = ScrobblingMangaAdapter(this, coil, this)
|
||||
with(binding.recyclerView) {
|
||||
adapter = listAdapter
|
||||
setHasFixedSize(true)
|
||||
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
|
||||
paddingHorizontal = spacing
|
||||
paddingVertical = resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)
|
||||
val decoration = TypedSpacingItemDecoration(
|
||||
FeedAdapter.ITEM_TYPE_FEED to 0,
|
||||
fallbackSpacing = spacing,
|
||||
)
|
||||
addItemDecoration(decoration)
|
||||
}
|
||||
binding.imageViewAvatar.setOnClickListener(this)
|
||||
|
||||
viewModel.content.observe(this, listAdapter::setItems)
|
||||
viewModel.user.observe(this, this::onUserChanged)
|
||||
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
||||
viewModel.onError.observe(this, this::onError)
|
||||
viewModel.onLoggedOut.observe(this) {
|
||||
finishAfterTransition()
|
||||
}
|
||||
|
||||
processIntent(intent)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
if (intent != null) {
|
||||
setIntent(intent)
|
||||
processIntent(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
binding.recyclerView.updatePadding(
|
||||
left = insets.left + paddingHorizontal,
|
||||
right = insets.right + paddingHorizontal,
|
||||
bottom = insets.bottom + paddingVertical,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: ScrobblingInfo, view: View) {
|
||||
startActivity(
|
||||
DetailsActivity.newIntent(this, item.mangaId),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.imageView_avatar -> showUserDialog()
|
||||
}
|
||||
}
|
||||
|
||||
private fun processIntent(intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_VIEW) {
|
||||
val uri = intent.data ?: return
|
||||
val code = uri.getQueryParameter("code")
|
||||
if (!code.isNullOrEmpty()) {
|
||||
viewModel.onAuthCodeReceived(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onUserChanged(user: ScrobblerUser?) {
|
||||
if (user == null) {
|
||||
binding.imageViewAvatar.disposeImageRequest()
|
||||
binding.imageViewAvatar.isVisible = false
|
||||
return
|
||||
}
|
||||
binding.imageViewAvatar.isVisible = true
|
||||
binding.imageViewAvatar.newImageRequest(user.avatar, null)
|
||||
?.enqueueWith(coil)
|
||||
}
|
||||
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
binding.progressBar.run {
|
||||
if (isLoading) {
|
||||
showCompat()
|
||||
} else {
|
||||
hideCompat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
Snackbar.make(
|
||||
binding.recyclerView,
|
||||
e.getDisplayMessage(resources),
|
||||
Snackbar.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun showUserDialog() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(title)
|
||||
.setMessage(getString(R.string.logged_in_as, viewModel.user.value?.nickname))
|
||||
.setNegativeButton(R.string.close, null)
|
||||
.setPositiveButton(R.string.logout) { _, _ ->
|
||||
viewModel.logout()
|
||||
}.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_SERVICE_ID = "service"
|
||||
|
||||
private const val HOST_SHIKIMORI_AUTH = "shikimori-auth"
|
||||
private const val HOST_ANILIST_AUTH = "anilist-auth"
|
||||
private const val HOST_MAL_AUTH = "mal-auth"
|
||||
|
||||
fun newIntent(context: Context, service: ScrobblerService) =
|
||||
Intent(context, ScrobblerConfigActivity::class.java)
|
||||
.putExtra(EXTRA_SERVICE_ID, service.id)
|
||||
|
||||
private fun getScrobblerService(
|
||||
intent: Intent
|
||||
): ScrobblerService? {
|
||||
val serviceId = intent.getIntExtra(EXTRA_SERVICE_ID, 0)
|
||||
if (serviceId != 0) {
|
||||
return enumValues<ScrobblerService>().first { it.id == serviceId }
|
||||
}
|
||||
val uri = intent.data ?: return null
|
||||
return when (uri.host) {
|
||||
HOST_SHIKIMORI_AUTH -> ScrobblerService.SHIKIMORI
|
||||
HOST_ANILIST_AUTH -> ScrobblerService.ANILIST
|
||||
HOST_MAL_AUTH -> ScrobblerService.MAL
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.config
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||
|
||||
class ScrobblerConfigViewModel @AssistedInject constructor(
|
||||
@Assisted scrobblerService: ScrobblerService,
|
||||
scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val scrobbler = scrobblers.first { it.scrobblerService == scrobblerService }
|
||||
|
||||
val titleResId = scrobbler.scrobblerService.titleResId
|
||||
|
||||
val user = MutableLiveData<ScrobblerUser?>(null)
|
||||
val onLoggedOut = SingleLiveEvent<Unit>()
|
||||
|
||||
val content = scrobbler.observeAllScrobblingInfo()
|
||||
.onStart { loadingCounter.increment() }
|
||||
.onFirst { loadingCounter.decrement() }
|
||||
.catch { errorEvent.postCall(it) }
|
||||
.map { buildContentList(it) }
|
||||
.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
||||
|
||||
init {
|
||||
scrobbler.user
|
||||
.onEach { user.postValue(it) }
|
||||
.launchIn(viewModelScope + Dispatchers.Default)
|
||||
}
|
||||
|
||||
fun onAuthCodeReceived(authCode: String) {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val newUser = scrobbler.authorize(authCode)
|
||||
user.postValue(newUser)
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
scrobbler.logout()
|
||||
user.postValue(null)
|
||||
onLoggedOut.postCall(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildContentList(list: List<ScrobblingInfo>): List<ListModel> {
|
||||
if (list.isEmpty()) {
|
||||
return listOf(
|
||||
EmptyState(
|
||||
icon = R.drawable.ic_empty_history,
|
||||
textPrimary = R.string.nothing_here,
|
||||
textSecondary = R.string.scrobbling_empty_hint,
|
||||
actionStringRes = 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
val grouped = list.groupBy { it.status }
|
||||
val statuses = enumValues<ScrobblingStatus>()
|
||||
val result = ArrayList<ListModel>(list.size + statuses.size)
|
||||
for (st in statuses) {
|
||||
val subList = grouped[st]
|
||||
if (subList.isNullOrEmpty()) {
|
||||
continue
|
||||
}
|
||||
result.add(st)
|
||||
result.addAll(subList)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
||||
fun create(service: ScrobblerService): ScrobblerConfigViewModel
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.config.adapter
|
||||
|
||||
import android.widget.TextView
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
|
||||
fun scrobblingHeaderAD() = adapterDelegate<ScrobblingStatus, ListModel>(R.layout.item_header) {
|
||||
|
||||
bind {
|
||||
(itemView as TextView).text = context.resources
|
||||
.getStringArray(R.array.scrobbling_statuses)
|
||||
.getOrNull(item.ordinal)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.config.adapter
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.databinding.ItemScrobblingMangaBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.scrobbling.common.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 scrobblingMangaAD(
|
||||
clickListener: OnListItemClickListener<ScrobblingInfo>,
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) = adapterDelegateViewBinding<ScrobblingInfo, ListModel, ItemScrobblingMangaBinding>(
|
||||
{ layoutInflater, parent -> ItemScrobblingMangaBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
val clickListenerAdapter = AdapterDelegateClickListenerAdapter(this, clickListener)
|
||||
itemView.setOnClickListener(clickListenerAdapter)
|
||||
|
||||
bind {
|
||||
binding.imageViewCover.newImageRequest(item.coverUrl, null)?.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.ratingBar.rating = item.rating * binding.ratingBar.numStars
|
||||
}
|
||||
|
||||
onViewRecycled {
|
||||
binding.imageViewCover.disposeImageRequest()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.config.adapter
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
|
||||
class ScrobblingMangaAdapter(
|
||||
clickListener: OnListItemClickListener<ScrobblingInfo>,
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
|
||||
|
||||
init {
|
||||
delegatesManager.addDelegate(scrobblingMangaAD(clickListener, coil, lifecycleOwner))
|
||||
.addDelegate(scrobblingHeaderAD())
|
||||
.addDelegate(emptyStateListAD(coil, null))
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||
return when {
|
||||
oldItem is ScrobblingInfo && newItem is ScrobblingInfo -> {
|
||||
oldItem.targetId == newItem.targetId && oldItem.mangaId == newItem.mangaId
|
||||
}
|
||||
|
||||
oldItem is ScrobblingStatus && newItem is ScrobblingStatus -> {
|
||||
oldItem.ordinal == newItem.ordinal
|
||||
}
|
||||
|
||||
oldItem is EmptyState && newItem is EmptyState -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
package org.koitharu.kotatsu.scrobbling.ui.selector.adapter
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.scrobbling.ui.selector.model.ScrobblerHint
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.model.ScrobblerHint
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
||||
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||
@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.ui.selector.model
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.selector.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
@ -1,11 +0,0 @@
|
||||
package org.koitharu.kotatsu.scrobbling.domain.model
|
||||
|
||||
enum class ScrobblingStatus {
|
||||
|
||||
PLANNED,
|
||||
READING,
|
||||
RE_READING,
|
||||
COMPLETED,
|
||||
ON_HOLD,
|
||||
DROPPED,
|
||||
}
|
||||
@ -1,86 +0,0 @@
|
||||
package org.koitharu.kotatsu.scrobbling.mal.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.CircleCropTransformation
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.utils.PreferenceIconTarget
|
||||
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MALSettingsFragment : BasePreferenceFragment(R.string.mal) {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: MALSettingsViewModel.Factory
|
||||
|
||||
private val viewModel by assistedViewModels {
|
||||
viewModelFactory.create(arguments?.getString(ARG_AUTH_CODE))
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_mal)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.user.observe(viewLifecycleOwner, this::onUserChanged)
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
KEY_USER -> openAuthorization()
|
||||
KEY_LOGOUT -> {
|
||||
viewModel.logout()
|
||||
true
|
||||
}
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onUserChanged(user: ScrobblerUser?) {
|
||||
val pref = findPreference<Preference>(KEY_USER) ?: return
|
||||
pref.isSelectable = user == null
|
||||
pref.title = user?.nickname ?: getString(R.string.sign_in)
|
||||
ImageRequest.Builder(requireContext())
|
||||
.data(user?.avatar)
|
||||
.transformations(CircleCropTransformation())
|
||||
.target(PreferenceIconTarget(pref))
|
||||
.enqueueWith(coil)
|
||||
findPreference<Preference>(KEY_LOGOUT)?.isVisible = user != null
|
||||
}
|
||||
|
||||
private fun openAuthorization(): Boolean {
|
||||
return runCatching {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse(viewModel.authorizationUrl)
|
||||
startActivity(intent)
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_USER = "mal_user"
|
||||
private const val KEY_LOGOUT = "mal_logout"
|
||||
|
||||
private const val ARG_AUTH_CODE = "auth_code"
|
||||
|
||||
fun newInstance(authCode: String?) = MALSettingsFragment().withArgs(1) {
|
||||
putString(ARG_AUTH_CODE, authCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
package org.koitharu.kotatsu.scrobbling.mal.ui
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository
|
||||
|
||||
class MALSettingsViewModel @AssistedInject constructor(
|
||||
private val repository: MALRepository,
|
||||
@Assisted authCode: String?,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val authorizationUrl: String
|
||||
get() = repository.oauthUrl
|
||||
|
||||
val user = MutableLiveData<ScrobblerUser?>()
|
||||
|
||||
init {
|
||||
if (authCode != null) {
|
||||
authorize(authCode)
|
||||
} else {
|
||||
loadUser()
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
repository.logout()
|
||||
user.postValue(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadUser() = launchJob(Dispatchers.Default) {
|
||||
val userModel = if (repository.isAuthorized) {
|
||||
repository.cachedUser?.let(user::postValue)
|
||||
repository.loadUser()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
user.postValue(userModel)
|
||||
}
|
||||
|
||||
private fun authorize(code: String) = launchJob(Dispatchers.Default) {
|
||||
repository.authorize(code)
|
||||
user.postValue(repository.loadUser())
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
||||
fun create(authCode: String?): MALSettingsViewModel
|
||||
}
|
||||
}
|
||||
@ -1,86 +0,0 @@
|
||||
package org.koitharu.kotatsu.scrobbling.shikimori.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.CircleCropTransformation
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.utils.PreferenceIconTarget
|
||||
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ShikimoriSettingsFragment : BasePreferenceFragment(R.string.shikimori) {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ShikimoriSettingsViewModel.Factory
|
||||
|
||||
private val viewModel by assistedViewModels {
|
||||
viewModelFactory.create(arguments?.getString(ARG_AUTH_CODE))
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_shikimori)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.user.observe(viewLifecycleOwner, this::onUserChanged)
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
KEY_USER -> openAuthorization()
|
||||
KEY_LOGOUT -> {
|
||||
viewModel.logout()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onUserChanged(user: ScrobblerUser?) {
|
||||
val pref = findPreference<Preference>(KEY_USER) ?: return
|
||||
pref.isSelectable = user == null
|
||||
pref.title = user?.nickname ?: getString(R.string.sign_in)
|
||||
ImageRequest.Builder(requireContext())
|
||||
.data(user?.avatar)
|
||||
.transformations(CircleCropTransformation())
|
||||
.target(PreferenceIconTarget(pref))
|
||||
.enqueueWith(coil)
|
||||
findPreference<Preference>(KEY_LOGOUT)?.isVisible = user != null
|
||||
}
|
||||
|
||||
private fun openAuthorization(): Boolean {
|
||||
return runCatching {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse(viewModel.authorizationUrl)
|
||||
startActivity(intent)
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_USER = "shiki_user"
|
||||
private const val KEY_LOGOUT = "shiki_logout"
|
||||
|
||||
private const val ARG_AUTH_CODE = "auth_code"
|
||||
|
||||
fun newInstance(authCode: String?) = ShikimoriSettingsFragment().withArgs(1) {
|
||||
putString(ARG_AUTH_CODE, authCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
package org.koitharu.kotatsu.scrobbling.shikimori.ui
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
|
||||
|
||||
class ShikimoriSettingsViewModel @AssistedInject constructor(
|
||||
private val repository: ShikimoriRepository,
|
||||
@Assisted authCode: String?,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val authorizationUrl: String
|
||||
get() = repository.oauthUrl
|
||||
|
||||
val user = MutableLiveData<ScrobblerUser?>()
|
||||
|
||||
init {
|
||||
if (authCode != null) {
|
||||
authorize(authCode)
|
||||
} else {
|
||||
loadUser()
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
repository.logout()
|
||||
user.postValue(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadUser() = launchJob(Dispatchers.Default) {
|
||||
val userModel = if (repository.isAuthorized) {
|
||||
repository.cachedUser?.let(user::postValue)
|
||||
repository.loadUser()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
user.postValue(userModel)
|
||||
}
|
||||
|
||||
private fun authorize(code: String) = launchJob(Dispatchers.Default) {
|
||||
repository.authorize(code)
|
||||
user.postValue(repository.loadUser())
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
||||
fun create(authCode: String?): ShikimoriSettingsViewModel
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 723 B After Width: | Height: | Size: 723 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<bitmap
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:src="@drawable/ic_shikimori_raw"
|
||||
android:tint="?colorControlNormal" />
|
||||
@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
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:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:id="@+id/collapsingToolbarLayout"
|
||||
style="?attr/collapsingToolbarLayoutMediumStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
|
||||
app:toolbarId="@id/toolbar">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_collapseMode="pin">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/imageView_avatar"
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:layout_marginHorizontal="@dimen/toolbar_button_margin"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:padding="1dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Circle"
|
||||
app:strokeColor="?colorOutline"
|
||||
app:strokeWidth="1dp"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="@dimen/list_spacing"
|
||||
android:paddingTop="@dimen/grid_spacing_outer"
|
||||
android:paddingRight="@dimen/list_spacing"
|
||||
android:paddingBottom="@dimen/grid_spacing_outer"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||
tools:listitem="@layout/item_scrobbling_manga" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:layout_anchor="@id/recyclerView"
|
||||
app:layout_anchorGravity="center"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/list_selector"
|
||||
android:clipChildren="false"
|
||||
android:padding="@dimen/list_spacing">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/imageView_cover"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
|
||||
tools:src="@sample/covers" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
app:layout_constraintBottom_toTopOf="@+id/ratingBar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
|
||||
app:layout_constraintTop_toTopOf="@+id/imageView_cover"
|
||||
tools:text="@sample/titles" />
|
||||
|
||||
<RatingBar
|
||||
android:id="@+id/ratingBar"
|
||||
style="?ratingBarStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:isIndicator="true"
|
||||
android:max="1"
|
||||
android:numStars="5"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/imageView_cover"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView_title" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
app:initialExpandedChildrenCount="5">
|
||||
|
||||
<Preference
|
||||
android:key="al_user"
|
||||
android:persistent="false"
|
||||
android:title="@string/loading_"
|
||||
app:iconSpaceReserved="true" />
|
||||
|
||||
<Preference
|
||||
android:key="al_logout"
|
||||
android:persistent="false"
|
||||
android:title="@string/logout"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
</PreferenceScreen>
|
||||
@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
app:initialExpandedChildrenCount="5">
|
||||
|
||||
<Preference
|
||||
android:key="mal_user"
|
||||
android:persistent="false"
|
||||
android:title="@string/loading_"
|
||||
app:iconSpaceReserved="true" />
|
||||
|
||||
<Preference
|
||||
android:key="mal_logout"
|
||||
android:persistent="false"
|
||||
android:title="@string/logout"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
</PreferenceScreen>
|
||||
@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
app:initialExpandedChildrenCount="5">
|
||||
|
||||
<Preference
|
||||
android:key="shiki_user"
|
||||
android:persistent="false"
|
||||
android:title="@string/loading_"
|
||||
app:iconSpaceReserved="true" />
|
||||
|
||||
<Preference
|
||||
android:key="shiki_logout"
|
||||
android:persistent="false"
|
||||
android:title="@string/logout"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
</PreferenceScreen>
|
||||