Cache shikimori user
This commit is contained in:
@@ -12,7 +12,7 @@ abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() {
|
||||
|
||||
private val bounds = Rect()
|
||||
private val boundsF = RectF()
|
||||
private val selection = HashSet<Long>()
|
||||
protected val selection = HashSet<Long>()
|
||||
|
||||
protected var hasBackground: Boolean = true
|
||||
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.parsers.model.MangaSource
|
||||
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.toUriOrNull
|
||||
|
||||
@@ -236,15 +237,7 @@ class AppSettings(context: Context) {
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
|
||||
fun observe() = callbackFlow<String> {
|
||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
trySendBlocking(key)
|
||||
}
|
||||
prefs.registerOnSharedPreferenceChangeListener(listener)
|
||||
awaitClose {
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
}
|
||||
fun observe() = prefs.observe()
|
||||
|
||||
companion object {
|
||||
|
||||
|
||||
@@ -18,17 +18,17 @@ import org.koitharu.kotatsu.utils.ext.getItem
|
||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||
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)
|
||||
private val checkIcon = ContextCompat.getDrawable(context, materialR.drawable.ic_mtrl_checked_circle)
|
||||
private val iconOffset = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)
|
||||
private val strokeColor = context.getThemeColor(materialR.attr.colorPrimary, Color.RED)
|
||||
private val fillColor = ColorUtils.setAlphaComponent(
|
||||
protected val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
protected val checkIcon = ContextCompat.getDrawable(context, materialR.drawable.ic_mtrl_checked_circle)
|
||||
protected val iconOffset = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)
|
||||
protected val strokeColor = context.getThemeColor(materialR.attr.colorPrimary, Color.RED)
|
||||
protected val fillColor = ColorUtils.setAlphaComponent(
|
||||
ColorUtils.blendARGB(strokeColor, context.getThemeColor(materialR.attr.colorSurface), 0.8f),
|
||||
0x74
|
||||
)
|
||||
private val defaultRadius = context.resources.getDimension(R.dimen.list_selector_corner)
|
||||
protected val defaultRadius = context.resources.getDimension(R.dimen.list_selector_corner)
|
||||
|
||||
init {
|
||||
hasBackground = false
|
||||
|
||||
@@ -5,6 +5,7 @@ import okhttp3.Authenticator
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
|
||||
class ShikimoriAuthenticator(
|
||||
@@ -38,9 +39,13 @@ class ShikimoriAuthenticator(
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun refreshAccessToken(): String? {
|
||||
private fun refreshAccessToken(): String? = runCatching {
|
||||
val repository = repositoryProvider()
|
||||
runBlocking { repository.authorize(null) }
|
||||
return storage.accessToken
|
||||
}
|
||||
}.onFailure {
|
||||
if (BuildConfig.DEBUG) {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
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.ShikimoriMangaInfo
|
||||
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_SECRET = "euBMt1GGRSDpVIFQVPxZrO7Kh6X4gWyv0dABuj4B-M8"
|
||||
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
|
||||
|
||||
class ShikimoriRepository(
|
||||
@@ -26,7 +28,7 @@ class ShikimoriRepository(
|
||||
) {
|
||||
|
||||
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="
|
||||
|
||||
val isAuthorized: Boolean
|
||||
@@ -45,7 +47,7 @@ class ShikimoriRepository(
|
||||
}
|
||||
val request = Request.Builder()
|
||||
.post(body.build())
|
||||
.url("https://shikimori.one/oauth/token")
|
||||
.url("${BASE_URL}oauth/token")
|
||||
val response = okHttp.newCall(request.build()).await().parseJson()
|
||||
storage.accessToken = response.getString("access_token")
|
||||
storage.refreshToken = response.getString("refresh_token")
|
||||
@@ -54,15 +56,24 @@ class ShikimoriRepository(
|
||||
suspend fun getUser(): ShikimoriUser {
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url("https://shikimori.one/api/users/whoami")
|
||||
.url("${BASE_URL}api/users/whoami")
|
||||
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> {
|
||||
val page = offset / MANGA_PAGE_SIZE
|
||||
val pageOffset = offset % MANGA_PAGE_SIZE
|
||||
val url = BASE_URL.toHttpUrl().newBuilder()
|
||||
.addPathSegment("api")
|
||||
.addPathSegment("mangas")
|
||||
.addEncodedQueryParameter("page", (page + 1).toString())
|
||||
.addEncodedQueryParameter("limit", MANGA_PAGE_SIZE.toString())
|
||||
@@ -75,11 +86,31 @@ class ShikimoriRepository(
|
||||
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? {
|
||||
val q = manga.title.urlEncoded()
|
||||
val request = Request.Builder()
|
||||
.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 candidates = response.mapJSON { ShikimoriManga(it) }
|
||||
val bestCandidate = candidates.filter {
|
||||
@@ -91,7 +122,7 @@ class ShikimoriRepository(
|
||||
suspend fun getRelatedManga(id: Long): List<ShikimoriManga> {
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url("https://shikimori.one/api/mangas/$id/related")
|
||||
.url("${BASE_URL}api/mangas/$id/related")
|
||||
val response = okHttp.newCall(request.build()).await().parseJsonArray()
|
||||
return response.mapJSON { jo -> ShikimoriManga(jo) }
|
||||
}
|
||||
@@ -99,7 +130,7 @@ class ShikimoriRepository(
|
||||
suspend fun getSimilarManga(id: Long): List<ShikimoriManga> {
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url("https://shikimori.one/api/mangas/$id/similar")
|
||||
.url("${BASE_URL}api/mangas/$id/similar")
|
||||
val response = okHttp.newCall(request.build()).await().parseJsonArray()
|
||||
return response.mapJSON { jo -> ShikimoriManga(jo) }
|
||||
}
|
||||
@@ -107,7 +138,7 @@ class ShikimoriRepository(
|
||||
suspend fun getMangaInfo(id: Long): ShikimoriMangaInfo {
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url("https://shikimori.one/api/mangas/$id")
|
||||
.url("${BASE_URL}api/mangas/$id")
|
||||
val response = okHttp.newCall(request.build()).await().parseJson()
|
||||
return ShikimoriMangaInfo(response)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ package org.koitharu.kotatsu.shikimori.data
|
||||
|
||||
import android.content.Context
|
||||
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 KEY_ACCESS_TOKEN = "access_token"
|
||||
private const val KEY_REFRESH_TOKEN = "refresh_token"
|
||||
private const val KEY_USER = "user"
|
||||
|
||||
class ShikimoriStorage(context: Context) {
|
||||
|
||||
@@ -18,4 +21,16 @@ class ShikimoriStorage(context: Context) {
|
||||
var refreshToken: String?
|
||||
get() = prefs.getString(KEY_REFRESH_TOKEN, null)
|
||||
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"),
|
||||
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 {
|
||||
return when (preference.key) {
|
||||
KEY_USER -> openAuthorization()
|
||||
KEY_LOGOUT -> {
|
||||
viewModel.logout()
|
||||
true
|
||||
}
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
@@ -50,6 +54,7 @@ class ShikimoriSettingsFragment : BasePreferenceFragment(R.string.shikimori) {
|
||||
.transformations(CircleCropTransformation())
|
||||
.target(PreferenceIconTarget(pref))
|
||||
.enqueueWith(coil)
|
||||
findPreference<Preference>(KEY_LOGOUT)?.isVisible = user != null
|
||||
}
|
||||
|
||||
private fun openAuthorization(): Boolean {
|
||||
@@ -63,6 +68,7 @@ class ShikimoriSettingsFragment : BasePreferenceFragment(R.string.shikimori) {
|
||||
companion object {
|
||||
|
||||
private const val KEY_USER = "shiki_user"
|
||||
private const val KEY_LOGOUT = "shiki_logout"
|
||||
|
||||
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) {
|
||||
val userModel = if (repository.isAuthorized) {
|
||||
repository.getCachedUser()?.let(user::postValue)
|
||||
repository.getUser()
|
||||
} else {
|
||||
null
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.koitharu.kotatsu.databinding.SheetShikiSelectorBinding
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
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.utils.BottomSheetToolbarController
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
@@ -30,7 +31,8 @@ import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
class ShikimoriSelectorBottomSheet :
|
||||
BaseBottomSheet<SheetShikiSelectorBinding>(),
|
||||
OnListItemClickListener<ShikimoriManga>,
|
||||
PaginationScrollListener.Callback, View.OnClickListener {
|
||||
PaginationScrollListener.Callback,
|
||||
View.OnClickListener {
|
||||
|
||||
private val viewModel by viewModel<ShikimoriSelectorViewModel> {
|
||||
parametersOf(requireNotNull(requireArguments().getParcelable<ParcelableManga>(MangaIntent.KEY_MANGA)).manga)
|
||||
@@ -46,13 +48,19 @@ class ShikimoriSelectorBottomSheet :
|
||||
binding.toolbar.setNavigationOnClickListener { dismiss() }
|
||||
addBottomSheetCallback(BottomSheetToolbarController(binding.toolbar))
|
||||
val listAdapter = ShikimoriSelectorAdapter(viewLifecycleOwner, get(), this)
|
||||
val decoration = ShikiMangaSelectionDecoration(view.context)
|
||||
with(binding.recyclerView) {
|
||||
adapter = listAdapter
|
||||
addItemDecoration(decoration)
|
||||
addOnScrollListener(PaginationScrollListener(4, this@ShikimoriSelectorBottomSheet))
|
||||
}
|
||||
binding.imageViewUser.setOnClickListener(this)
|
||||
|
||||
viewModel.content.observe(viewLifecycleOwner) { listAdapter.items = it }
|
||||
viewModel.selectedItemId.observe(viewLifecycleOwner) {
|
||||
decoration.checkedItemId = it
|
||||
binding.recyclerView.invalidateItemDecorations()
|
||||
}
|
||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
||||
viewModel.avatar.observe(viewLifecycleOwner, ::setUserAvatar)
|
||||
}
|
||||
@@ -64,6 +72,7 @@ class ShikimoriSelectorBottomSheet :
|
||||
}
|
||||
|
||||
override fun onItemClick(item: ShikimoriManga, view: View) {
|
||||
viewModel.selectedItemId.value = item.id
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package org.koitharu.kotatsu.shikimori.ui.selector
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -37,7 +39,10 @@ class ShikimoriSelectorViewModel(
|
||||
}
|
||||
}.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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.ResolveInfo
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
@@ -10,8 +11,14 @@ import android.os.Build
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.work.CoroutineWorker
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
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
|
||||
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
@@ -55,4 +62,23 @@ fun <I> ActivityResultLauncher<I>.tryLaunch(input: I, options: ActivityOptionsCo
|
||||
return runCatching {
|
||||
launch(input, options)
|
||||
}.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="tracking">Tracking</string>
|
||||
<string name="empty_favourite_categories">No favourite categories</string>
|
||||
<string name="logout">Logout</string>
|
||||
</resources>
|
||||
@@ -10,4 +10,10 @@
|
||||
android:title="@string/loading_"
|
||||
app:iconSpaceReserved="true" />
|
||||
|
||||
<Preference
|
||||
android:key="shiki_logout"
|
||||
android:persistent="false"
|
||||
android:title="@string/logout"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
Reference in New Issue
Block a user