Cache shikimori user

This commit is contained in:
Koitharu
2022-05-09 16:24:24 +03:00
parent c2ba716916
commit 82efa8298d
16 changed files with 225 additions and 32 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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()
} }

View File

@@ -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)
} }

View File

@@ -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()
}
} }

View File

@@ -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
}
} }

View File

@@ -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"

View File

@@ -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

View File

@@ -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() {

View File

@@ -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())
} }

View File

@@ -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)
}
}
}

View File

@@ -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()

View 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)

View File

@@ -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>

View File

@@ -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>