Scrobblers config activity
@ -1,3 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.model
|
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
|
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
|
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.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
|
||||||
|
|
||||||
interface ScrobblerRepository {
|
interface ScrobblerRepository {
|
||||||
|
|
||||||
@ -1,10 +1,10 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.data
|
package org.koitharu.kotatsu.scrobbling.common.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import org.jsoup.internal.StringUtil.StringJoiner
|
import org.jsoup.internal.StringUtil.StringJoiner
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
|
||||||
|
|
||||||
private const val KEY_ACCESS_TOKEN = "access_token"
|
private const val KEY_ACCESS_TOKEN = "access_token"
|
||||||
private const val KEY_REFRESH_TOKEN = "refresh_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.ColumnInfo
|
||||||
import androidx.room.Entity
|
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
|
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(
|
class ScrobblerMangaInfo(
|
||||||
val id: Long,
|
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.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
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
|
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(
|
class ScrobblerUser(
|
||||||
val id: Long,
|
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 com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding
|
import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
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.getDisplayMessage
|
||||||
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
||||||
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
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.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
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>
|
|
||||||