Cache shikimori user
This commit is contained in:
@@ -12,7 +12,7 @@ abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() {
|
|||||||
|
|
||||||
private val bounds = Rect()
|
private val bounds = Rect()
|
||||||
private val boundsF = RectF()
|
private val boundsF = RectF()
|
||||||
private val selection = HashSet<Long>()
|
protected val selection = HashSet<Long>()
|
||||||
|
|
||||||
protected var hasBackground: Boolean = true
|
protected var hasBackground: Boolean = true
|
||||||
protected var hasForeground: Boolean = false
|
protected var hasForeground: Boolean = false
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import org.koitharu.kotatsu.BuildConfig
|
|||||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.utils.ext.getEnumValue
|
import org.koitharu.kotatsu.utils.ext.getEnumValue
|
||||||
|
import org.koitharu.kotatsu.utils.ext.observe
|
||||||
import org.koitharu.kotatsu.utils.ext.putEnumValue
|
import org.koitharu.kotatsu.utils.ext.putEnumValue
|
||||||
import org.koitharu.kotatsu.utils.ext.toUriOrNull
|
import org.koitharu.kotatsu.utils.ext.toUriOrNull
|
||||||
|
|
||||||
@@ -236,15 +237,7 @@ class AppSettings(context: Context) {
|
|||||||
prefs.unregisterOnSharedPreferenceChangeListener(listener)
|
prefs.unregisterOnSharedPreferenceChangeListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observe() = callbackFlow<String> {
|
fun observe() = prefs.observe()
|
||||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
|
||||||
trySendBlocking(key)
|
|
||||||
}
|
|
||||||
prefs.registerOnSharedPreferenceChangeListener(listener)
|
|
||||||
awaitClose {
|
|
||||||
prefs.unregisterOnSharedPreferenceChangeListener(listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
|||||||
@@ -18,17 +18,17 @@ import org.koitharu.kotatsu.utils.ext.getItem
|
|||||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
class MangaSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() {
|
open class MangaSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() {
|
||||||
|
|
||||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
protected val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
private val checkIcon = ContextCompat.getDrawable(context, materialR.drawable.ic_mtrl_checked_circle)
|
protected val checkIcon = ContextCompat.getDrawable(context, materialR.drawable.ic_mtrl_checked_circle)
|
||||||
private val iconOffset = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)
|
protected val iconOffset = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)
|
||||||
private val strokeColor = context.getThemeColor(materialR.attr.colorPrimary, Color.RED)
|
protected val strokeColor = context.getThemeColor(materialR.attr.colorPrimary, Color.RED)
|
||||||
private val fillColor = ColorUtils.setAlphaComponent(
|
protected val fillColor = ColorUtils.setAlphaComponent(
|
||||||
ColorUtils.blendARGB(strokeColor, context.getThemeColor(materialR.attr.colorSurface), 0.8f),
|
ColorUtils.blendARGB(strokeColor, context.getThemeColor(materialR.attr.colorSurface), 0.8f),
|
||||||
0x74
|
0x74
|
||||||
)
|
)
|
||||||
private val defaultRadius = context.resources.getDimension(R.dimen.list_selector_corner)
|
protected val defaultRadius = context.resources.getDimension(R.dimen.list_selector_corner)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
hasBackground = false
|
hasBackground = false
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import okhttp3.Authenticator
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.Route
|
import okhttp3.Route
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||||
|
|
||||||
class ShikimoriAuthenticator(
|
class ShikimoriAuthenticator(
|
||||||
@@ -38,9 +39,13 @@ class ShikimoriAuthenticator(
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshAccessToken(): String? {
|
private fun refreshAccessToken(): String? = runCatching {
|
||||||
val repository = repositoryProvider()
|
val repository = repositoryProvider()
|
||||||
runBlocking { repository.authorize(null) }
|
runBlocking { repository.authorize(null) }
|
||||||
return storage.accessToken
|
return storage.accessToken
|
||||||
}
|
}.onFailure {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
it.printStackTrace()
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import okhttp3.FormBody
|
|||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import org.json.JSONObject
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.util.await
|
import org.koitharu.kotatsu.parsers.util.await
|
||||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||||
@@ -13,11 +14,12 @@ import org.koitharu.kotatsu.parsers.util.urlEncoded
|
|||||||
import org.koitharu.kotatsu.shikimori.data.model.ShikimoriManga
|
import org.koitharu.kotatsu.shikimori.data.model.ShikimoriManga
|
||||||
import org.koitharu.kotatsu.shikimori.data.model.ShikimoriMangaInfo
|
import org.koitharu.kotatsu.shikimori.data.model.ShikimoriMangaInfo
|
||||||
import org.koitharu.kotatsu.shikimori.data.model.ShikimoriUser
|
import org.koitharu.kotatsu.shikimori.data.model.ShikimoriUser
|
||||||
|
import org.koitharu.kotatsu.utils.ext.toRequestBody
|
||||||
|
|
||||||
private const val CLIENT_ID = "Mw6F0tPEOgyV7F9U9Twg50Q8SndMY7hzIOfXg0AX_XU"
|
private const val CLIENT_ID = "Mw6F0tPEOgyV7F9U9Twg50Q8SndMY7hzIOfXg0AX_XU"
|
||||||
private const val CLIENT_SECRET = "euBMt1GGRSDpVIFQVPxZrO7Kh6X4gWyv0dABuj4B-M8"
|
private const val CLIENT_SECRET = "euBMt1GGRSDpVIFQVPxZrO7Kh6X4gWyv0dABuj4B-M8"
|
||||||
private const val REDIRECT_URI = "kotatsu://shikimori-auth"
|
private const val REDIRECT_URI = "kotatsu://shikimori-auth"
|
||||||
private const val BASE_URL = "https://shikimori.one/api/"
|
private const val BASE_URL = "https://shikimori.one/"
|
||||||
private const val MANGA_PAGE_SIZE = 10
|
private const val MANGA_PAGE_SIZE = 10
|
||||||
|
|
||||||
class ShikimoriRepository(
|
class ShikimoriRepository(
|
||||||
@@ -26,7 +28,7 @@ class ShikimoriRepository(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
val oauthUrl: String
|
val oauthUrl: String
|
||||||
get() = "https://shikimori.one/oauth/authorize?client_id=$CLIENT_ID&" +
|
get() = "${BASE_URL}oauth/authorize?client_id=$CLIENT_ID&" +
|
||||||
"redirect_uri=$REDIRECT_URI&response_type=code&scope="
|
"redirect_uri=$REDIRECT_URI&response_type=code&scope="
|
||||||
|
|
||||||
val isAuthorized: Boolean
|
val isAuthorized: Boolean
|
||||||
@@ -45,7 +47,7 @@ class ShikimoriRepository(
|
|||||||
}
|
}
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.post(body.build())
|
.post(body.build())
|
||||||
.url("https://shikimori.one/oauth/token")
|
.url("${BASE_URL}oauth/token")
|
||||||
val response = okHttp.newCall(request.build()).await().parseJson()
|
val response = okHttp.newCall(request.build()).await().parseJson()
|
||||||
storage.accessToken = response.getString("access_token")
|
storage.accessToken = response.getString("access_token")
|
||||||
storage.refreshToken = response.getString("refresh_token")
|
storage.refreshToken = response.getString("refresh_token")
|
||||||
@@ -54,15 +56,24 @@ class ShikimoriRepository(
|
|||||||
suspend fun getUser(): ShikimoriUser {
|
suspend fun getUser(): ShikimoriUser {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.get()
|
.get()
|
||||||
.url("https://shikimori.one/api/users/whoami")
|
.url("${BASE_URL}api/users/whoami")
|
||||||
val response = okHttp.newCall(request.build()).await().parseJson()
|
val response = okHttp.newCall(request.build()).await().parseJson()
|
||||||
return ShikimoriUser(response)
|
return ShikimoriUser(response).also { storage.user = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCachedUser(): ShikimoriUser? {
|
||||||
|
return storage.user
|
||||||
|
}
|
||||||
|
|
||||||
|
fun logout() {
|
||||||
|
storage.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun findManga(query: String, offset: Int): List<ShikimoriManga> {
|
suspend fun findManga(query: String, offset: Int): List<ShikimoriManga> {
|
||||||
val page = offset / MANGA_PAGE_SIZE
|
val page = offset / MANGA_PAGE_SIZE
|
||||||
val pageOffset = offset % MANGA_PAGE_SIZE
|
val pageOffset = offset % MANGA_PAGE_SIZE
|
||||||
val url = BASE_URL.toHttpUrl().newBuilder()
|
val url = BASE_URL.toHttpUrl().newBuilder()
|
||||||
|
.addPathSegment("api")
|
||||||
.addPathSegment("mangas")
|
.addPathSegment("mangas")
|
||||||
.addEncodedQueryParameter("page", (page + 1).toString())
|
.addEncodedQueryParameter("page", (page + 1).toString())
|
||||||
.addEncodedQueryParameter("limit", MANGA_PAGE_SIZE.toString())
|
.addEncodedQueryParameter("limit", MANGA_PAGE_SIZE.toString())
|
||||||
@@ -75,11 +86,31 @@ class ShikimoriRepository(
|
|||||||
return if (pageOffset != 0) list.drop(pageOffset) else list
|
return if (pageOffset != 0) list.drop(pageOffset) else list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun trackManga(manga: Manga, shikiMangaId: Long) {
|
||||||
|
val user = getCachedUser() ?: getUser()
|
||||||
|
val payload = JSONObject()
|
||||||
|
payload.put(
|
||||||
|
"user_rate",
|
||||||
|
JSONObject().apply {
|
||||||
|
put("target_id", shikiMangaId)
|
||||||
|
put("target_type", "Manga")
|
||||||
|
put("user_id", user.id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val url = BASE_URL.toHttpUrl().newBuilder()
|
||||||
|
.addPathSegment("api")
|
||||||
|
.addPathSegment("v2")
|
||||||
|
.addPathSegment("user_rates")
|
||||||
|
.build()
|
||||||
|
val request = Request.Builder().url(url).post(payload.toRequestBody()).build()
|
||||||
|
val response = okHttp.newCall(request).await().parseJson()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun findMangaInfo(manga: Manga): ShikimoriMangaInfo? {
|
suspend fun findMangaInfo(manga: Manga): ShikimoriMangaInfo? {
|
||||||
val q = manga.title.urlEncoded()
|
val q = manga.title.urlEncoded()
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.get()
|
.get()
|
||||||
.url("https://shikimori.one/api/mangas?limit=5&search=$q&censored=false")
|
.url("${BASE_URL}api/mangas?limit=5&search=$q&censored=false")
|
||||||
val response = okHttp.newCall(request.build()).await().parseJsonArray()
|
val response = okHttp.newCall(request.build()).await().parseJsonArray()
|
||||||
val candidates = response.mapJSON { ShikimoriManga(it) }
|
val candidates = response.mapJSON { ShikimoriManga(it) }
|
||||||
val bestCandidate = candidates.filter {
|
val bestCandidate = candidates.filter {
|
||||||
@@ -91,7 +122,7 @@ class ShikimoriRepository(
|
|||||||
suspend fun getRelatedManga(id: Long): List<ShikimoriManga> {
|
suspend fun getRelatedManga(id: Long): List<ShikimoriManga> {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.get()
|
.get()
|
||||||
.url("https://shikimori.one/api/mangas/$id/related")
|
.url("${BASE_URL}api/mangas/$id/related")
|
||||||
val response = okHttp.newCall(request.build()).await().parseJsonArray()
|
val response = okHttp.newCall(request.build()).await().parseJsonArray()
|
||||||
return response.mapJSON { jo -> ShikimoriManga(jo) }
|
return response.mapJSON { jo -> ShikimoriManga(jo) }
|
||||||
}
|
}
|
||||||
@@ -99,7 +130,7 @@ class ShikimoriRepository(
|
|||||||
suspend fun getSimilarManga(id: Long): List<ShikimoriManga> {
|
suspend fun getSimilarManga(id: Long): List<ShikimoriManga> {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.get()
|
.get()
|
||||||
.url("https://shikimori.one/api/mangas/$id/similar")
|
.url("${BASE_URL}api/mangas/$id/similar")
|
||||||
val response = okHttp.newCall(request.build()).await().parseJsonArray()
|
val response = okHttp.newCall(request.build()).await().parseJsonArray()
|
||||||
return response.mapJSON { jo -> ShikimoriManga(jo) }
|
return response.mapJSON { jo -> ShikimoriManga(jo) }
|
||||||
}
|
}
|
||||||
@@ -107,7 +138,7 @@ class ShikimoriRepository(
|
|||||||
suspend fun getMangaInfo(id: Long): ShikimoriMangaInfo {
|
suspend fun getMangaInfo(id: Long): ShikimoriMangaInfo {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.get()
|
.get()
|
||||||
.url("https://shikimori.one/api/mangas/$id")
|
.url("${BASE_URL}api/mangas/$id")
|
||||||
val response = okHttp.newCall(request.build()).await().parseJson()
|
val response = okHttp.newCall(request.build()).await().parseJson()
|
||||||
return ShikimoriMangaInfo(response)
|
return ShikimoriMangaInfo(response)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ package org.koitharu.kotatsu.shikimori.data
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.koitharu.kotatsu.shikimori.data.model.ShikimoriUser
|
||||||
|
|
||||||
private const val PREF_NAME = "shikimori"
|
private const val PREF_NAME = "shikimori"
|
||||||
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"
|
||||||
|
private const val KEY_USER = "user"
|
||||||
|
|
||||||
class ShikimoriStorage(context: Context) {
|
class ShikimoriStorage(context: Context) {
|
||||||
|
|
||||||
@@ -18,4 +21,16 @@ class ShikimoriStorage(context: Context) {
|
|||||||
var refreshToken: String?
|
var refreshToken: String?
|
||||||
get() = prefs.getString(KEY_REFRESH_TOKEN, null)
|
get() = prefs.getString(KEY_REFRESH_TOKEN, null)
|
||||||
set(value) = prefs.edit { putString(KEY_REFRESH_TOKEN, value) }
|
set(value) = prefs.edit { putString(KEY_REFRESH_TOKEN, value) }
|
||||||
|
|
||||||
|
var user: ShikimoriUser?
|
||||||
|
get() = prefs.getString(KEY_USER, null)?.let {
|
||||||
|
ShikimoriUser(JSONObject(it))
|
||||||
|
}
|
||||||
|
set(value) = prefs.edit {
|
||||||
|
putString(KEY_USER, value?.toJson()?.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() = prefs.edit {
|
||||||
|
clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -13,4 +13,30 @@ class ShikimoriUser(
|
|||||||
nickname = json.getString("nickname"),
|
nickname = json.getString("nickname"),
|
||||||
avatar = json.getString("avatar"),
|
avatar = json.getString("avatar"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun toJson() = JSONObject().apply {
|
||||||
|
put("id", id)
|
||||||
|
put("nickname", nickname)
|
||||||
|
put("avatar", avatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as ShikimoriUser
|
||||||
|
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (nickname != other.nickname) return false
|
||||||
|
if (avatar != other.avatar) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = id.hashCode()
|
||||||
|
result = 31 * result + nickname.hashCode()
|
||||||
|
result = 31 * result + avatar.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -37,6 +37,10 @@ class ShikimoriSettingsFragment : BasePreferenceFragment(R.string.shikimori) {
|
|||||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||||
return when (preference.key) {
|
return when (preference.key) {
|
||||||
KEY_USER -> openAuthorization()
|
KEY_USER -> openAuthorization()
|
||||||
|
KEY_LOGOUT -> {
|
||||||
|
viewModel.logout()
|
||||||
|
true
|
||||||
|
}
|
||||||
else -> super.onPreferenceTreeClick(preference)
|
else -> super.onPreferenceTreeClick(preference)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,6 +54,7 @@ class ShikimoriSettingsFragment : BasePreferenceFragment(R.string.shikimori) {
|
|||||||
.transformations(CircleCropTransformation())
|
.transformations(CircleCropTransformation())
|
||||||
.target(PreferenceIconTarget(pref))
|
.target(PreferenceIconTarget(pref))
|
||||||
.enqueueWith(coil)
|
.enqueueWith(coil)
|
||||||
|
findPreference<Preference>(KEY_LOGOUT)?.isVisible = user != null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openAuthorization(): Boolean {
|
private fun openAuthorization(): Boolean {
|
||||||
@@ -63,6 +68,7 @@ class ShikimoriSettingsFragment : BasePreferenceFragment(R.string.shikimori) {
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val KEY_USER = "shiki_user"
|
private const val KEY_USER = "shiki_user"
|
||||||
|
private const val KEY_LOGOUT = "shiki_logout"
|
||||||
|
|
||||||
private const val ARG_AUTH_CODE = "auth_code"
|
private const val ARG_AUTH_CODE = "auth_code"
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,16 @@ class ShikimoriSettingsViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun logout() {
|
||||||
|
launchJob(Dispatchers.Default) {
|
||||||
|
repository.logout()
|
||||||
|
user.postValue(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadUser() = launchJob(Dispatchers.Default) {
|
private fun loadUser() = launchJob(Dispatchers.Default) {
|
||||||
val userModel = if (repository.isAuthorized) {
|
val userModel = if (repository.isAuthorized) {
|
||||||
|
repository.getCachedUser()?.let(user::postValue)
|
||||||
repository.getUser()
|
repository.getUser()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import org.koitharu.kotatsu.databinding.SheetShikiSelectorBinding
|
|||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||||
import org.koitharu.kotatsu.shikimori.data.model.ShikimoriManga
|
import org.koitharu.kotatsu.shikimori.data.model.ShikimoriManga
|
||||||
|
import org.koitharu.kotatsu.shikimori.ui.selector.adapter.ShikiMangaSelectionDecoration
|
||||||
import org.koitharu.kotatsu.shikimori.ui.selector.adapter.ShikimoriSelectorAdapter
|
import org.koitharu.kotatsu.shikimori.ui.selector.adapter.ShikimoriSelectorAdapter
|
||||||
import org.koitharu.kotatsu.utils.BottomSheetToolbarController
|
import org.koitharu.kotatsu.utils.BottomSheetToolbarController
|
||||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
@@ -30,7 +31,8 @@ import org.koitharu.kotatsu.utils.ext.withArgs
|
|||||||
class ShikimoriSelectorBottomSheet :
|
class ShikimoriSelectorBottomSheet :
|
||||||
BaseBottomSheet<SheetShikiSelectorBinding>(),
|
BaseBottomSheet<SheetShikiSelectorBinding>(),
|
||||||
OnListItemClickListener<ShikimoriManga>,
|
OnListItemClickListener<ShikimoriManga>,
|
||||||
PaginationScrollListener.Callback, View.OnClickListener {
|
PaginationScrollListener.Callback,
|
||||||
|
View.OnClickListener {
|
||||||
|
|
||||||
private val viewModel by viewModel<ShikimoriSelectorViewModel> {
|
private val viewModel by viewModel<ShikimoriSelectorViewModel> {
|
||||||
parametersOf(requireNotNull(requireArguments().getParcelable<ParcelableManga>(MangaIntent.KEY_MANGA)).manga)
|
parametersOf(requireNotNull(requireArguments().getParcelable<ParcelableManga>(MangaIntent.KEY_MANGA)).manga)
|
||||||
@@ -46,13 +48,19 @@ class ShikimoriSelectorBottomSheet :
|
|||||||
binding.toolbar.setNavigationOnClickListener { dismiss() }
|
binding.toolbar.setNavigationOnClickListener { dismiss() }
|
||||||
addBottomSheetCallback(BottomSheetToolbarController(binding.toolbar))
|
addBottomSheetCallback(BottomSheetToolbarController(binding.toolbar))
|
||||||
val listAdapter = ShikimoriSelectorAdapter(viewLifecycleOwner, get(), this)
|
val listAdapter = ShikimoriSelectorAdapter(viewLifecycleOwner, get(), this)
|
||||||
|
val decoration = ShikiMangaSelectionDecoration(view.context)
|
||||||
with(binding.recyclerView) {
|
with(binding.recyclerView) {
|
||||||
adapter = listAdapter
|
adapter = listAdapter
|
||||||
|
addItemDecoration(decoration)
|
||||||
addOnScrollListener(PaginationScrollListener(4, this@ShikimoriSelectorBottomSheet))
|
addOnScrollListener(PaginationScrollListener(4, this@ShikimoriSelectorBottomSheet))
|
||||||
}
|
}
|
||||||
binding.imageViewUser.setOnClickListener(this)
|
binding.imageViewUser.setOnClickListener(this)
|
||||||
|
|
||||||
viewModel.content.observe(viewLifecycleOwner) { listAdapter.items = it }
|
viewModel.content.observe(viewLifecycleOwner) { listAdapter.items = it }
|
||||||
|
viewModel.selectedItemId.observe(viewLifecycleOwner) {
|
||||||
|
decoration.checkedItemId = it
|
||||||
|
binding.recyclerView.invalidateItemDecorations()
|
||||||
|
}
|
||||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
||||||
viewModel.avatar.observe(viewLifecycleOwner, ::setUserAvatar)
|
viewModel.avatar.observe(viewLifecycleOwner, ::setUserAvatar)
|
||||||
}
|
}
|
||||||
@@ -64,6 +72,7 @@ class ShikimoriSelectorBottomSheet :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: ShikimoriManga, view: View) {
|
override fun onItemClick(item: ShikimoriManga, view: View) {
|
||||||
|
viewModel.selectedItemId.value = item.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScrolledToEnd() {
|
override fun onScrolledToEnd() {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package org.koitharu.kotatsu.shikimori.ui.selector
|
package org.koitharu.kotatsu.shikimori.ui.selector
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.liveData
|
import androidx.lifecycle.liveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -37,7 +39,10 @@ class ShikimoriSelectorViewModel(
|
|||||||
}
|
}
|
||||||
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
||||||
|
|
||||||
val avatar = liveData {
|
val selectedItemId = MutableLiveData(RecyclerView.NO_ID)
|
||||||
|
|
||||||
|
val avatar = liveData(viewModelScope.coroutineContext + Dispatchers.Default) {
|
||||||
|
emit(repository.getCachedUser()?.avatar)
|
||||||
emit(runCatching { repository.getUser().avatar }.getOrNull())
|
emit(runCatching { repository.getUser().avatar }.getOrNull())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package org.koitharu.kotatsu.shikimori.ui.selector.adapter
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
||||||
|
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
||||||
|
import org.koitharu.kotatsu.shikimori.data.model.ShikimoriManga
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getItem
|
||||||
|
|
||||||
|
class ShikiMangaSelectionDecoration(context: Context) : MangaSelectionDecoration(context) {
|
||||||
|
|
||||||
|
var checkedItemId: Long
|
||||||
|
get() = selection.singleOrNull() ?: NO_ID
|
||||||
|
set(value) {
|
||||||
|
clearSelection()
|
||||||
|
if (value != NO_ID) {
|
||||||
|
selection.add(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemId(parent: RecyclerView, child: View): Long {
|
||||||
|
val holder = parent.getChildViewHolder(child) ?: return NO_ID
|
||||||
|
val item = holder.getItem(ShikimoriManga::class.java) ?: return NO_ID
|
||||||
|
return item.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDrawForeground(
|
||||||
|
canvas: Canvas,
|
||||||
|
parent: RecyclerView,
|
||||||
|
child: View,
|
||||||
|
bounds: RectF,
|
||||||
|
state: RecyclerView.State,
|
||||||
|
) {
|
||||||
|
paint.color = strokeColor
|
||||||
|
paint.style = Paint.Style.STROKE
|
||||||
|
canvas.drawRoundRect(bounds, defaultRadius, defaultRadius, paint)
|
||||||
|
checkIcon?.run {
|
||||||
|
val offset = (bounds.height() - intrinsicHeight) / 2
|
||||||
|
setBounds(
|
||||||
|
(bounds.right - offset - intrinsicWidth).toInt(),
|
||||||
|
(bounds.top + offset).toInt(),
|
||||||
|
(bounds.right - offset).toInt(),
|
||||||
|
(bounds.top + offset + intrinsicHeight).toInt(),
|
||||||
|
)
|
||||||
|
draw(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.koitharu.kotatsu.utils.ext
|
package org.koitharu.kotatsu.utils.ext
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.ResolveInfo
|
import android.content.pm.ResolveInfo
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.Network
|
import android.net.Network
|
||||||
@@ -10,8 +11,14 @@ import android.os.Build
|
|||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
|
||||||
val Context.connectivityManager: ConnectivityManager
|
val Context.connectivityManager: ConnectivityManager
|
||||||
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
@@ -55,4 +62,23 @@ fun <I> ActivityResultLauncher<I>.tryLaunch(input: I, options: ActivityOptionsCo
|
|||||||
return runCatching {
|
return runCatching {
|
||||||
launch(input, options)
|
launch(input, options)
|
||||||
}.isSuccess
|
}.isSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun SharedPreferences.observe() = callbackFlow<String> {
|
||||||
|
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||||
|
trySendBlocking(key)
|
||||||
|
}
|
||||||
|
registerOnSharedPreferenceChangeListener(listener)
|
||||||
|
awaitClose {
|
||||||
|
unregisterOnSharedPreferenceChangeListener(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> SharedPreferences.observe(key: String, valueProducer: suspend () -> T): Flow<T> = flow {
|
||||||
|
emit(valueProducer())
|
||||||
|
observe().collect { upstreamKey ->
|
||||||
|
if (upstreamKey == key) {
|
||||||
|
emit(valueProducer())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.distinctUntilChanged()
|
||||||
10
app/src/main/java/org/koitharu/kotatsu/utils/ext/HttpExt.kt
Normal file
10
app/src/main/java/org/koitharu/kotatsu/utils/ext/HttpExt.kt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
package org.koitharu.kotatsu.utils.ext
|
||||||
|
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
private val TYPE_JSON = "application/json".toMediaType()
|
||||||
|
|
||||||
|
fun JSONObject.toRequestBody() = toString().toRequestBody(TYPE_JSON)
|
||||||
@@ -291,4 +291,5 @@
|
|||||||
<string name="edit_category">Edit category</string>
|
<string name="edit_category">Edit category</string>
|
||||||
<string name="tracking">Tracking</string>
|
<string name="tracking">Tracking</string>
|
||||||
<string name="empty_favourite_categories">No favourite categories</string>
|
<string name="empty_favourite_categories">No favourite categories</string>
|
||||||
|
<string name="logout">Logout</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -10,4 +10,10 @@
|
|||||||
android:title="@string/loading_"
|
android:title="@string/loading_"
|
||||||
app:iconSpaceReserved="true" />
|
app:iconSpaceReserved="true" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="shiki_logout"
|
||||||
|
android:persistent="false"
|
||||||
|
android:title="@string/logout"
|
||||||
|
app:allowDividerAbove="true" />
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
|||||||
Reference in New Issue
Block a user