From 82efa8298d605140dcf41fedf092a6d65a001522 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 9 May 2022 16:24:24 +0300 Subject: [PATCH] Cache shikimori user --- .../decor/AbstractSelectionItemDecoration.kt | 2 +- .../kotatsu/core/prefs/AppSettings.kt | 11 +--- .../list/ui/MangaSelectionDecoration.kt | 14 ++--- .../shikimori/data/ShikimoriAuthenticator.kt | 9 +++- .../shikimori/data/ShikimoriRepository.kt | 49 +++++++++++++---- .../shikimori/data/ShikimoriStorage.kt | 15 ++++++ .../shikimori/data/model/ShikimoriUser.kt | 26 ++++++++++ .../shikimori/ui/ShikimoriSettingsFragment.kt | 6 +++ .../ui/ShikimoriSettingsViewModel.kt | 8 +++ .../selector/ShikimoriSelectorBottomSheet.kt | 11 +++- .../ui/selector/ShikimoriSelectorViewModel.kt | 7 ++- .../adapter/ShikiMangaSelectionDecoration.kt | 52 +++++++++++++++++++ .../koitharu/kotatsu/utils/ext/AndroidExt.kt | 30 ++++++++++- .../org/koitharu/kotatsu/utils/ext/HttpExt.kt | 10 ++++ app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/pref_shikimori.xml | 6 +++ 16 files changed, 225 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/adapter/ShikiMangaSelectionDecoration.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/ext/HttpExt.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/AbstractSelectionItemDecoration.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/AbstractSelectionItemDecoration.kt index ac624d3c6..1974f6a5d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/AbstractSelectionItemDecoration.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/AbstractSelectionItemDecoration.kt @@ -12,7 +12,7 @@ abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() { private val bounds = Rect() private val boundsF = RectF() - private val selection = HashSet() + protected val selection = HashSet() protected var hasBackground: Boolean = true protected var hasForeground: Boolean = false diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index ac70b392a..a3ebc7f7a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -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 { - val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> - trySendBlocking(key) - } - prefs.registerOnSharedPreferenceChangeListener(listener) - awaitClose { - prefs.unregisterOnSharedPreferenceChangeListener(listener) - } - } + fun observe() = prefs.observe() companion object { diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaSelectionDecoration.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaSelectionDecoration.kt index 3ef530824..8422d00ec 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaSelectionDecoration.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaSelectionDecoration.kt @@ -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 diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriAuthenticator.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriAuthenticator.kt index 6a73b5829..6bf1381c2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriAuthenticator.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriAuthenticator.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt index 2cfca1505..5accdf614 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt @@ -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 { 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 { 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 { 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) } diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriStorage.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriStorage.kt index 2edc946ba..210432670 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriStorage.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriStorage.kt @@ -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() + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriUser.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriUser.kt index eff1464e6..d42188a63 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriUser.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriUser.kt @@ -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 + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsFragment.kt index 27fa2ed7a..aa7cbc6a4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsFragment.kt @@ -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(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" diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsViewModel.kt index 3fd4186d5..88e62f08d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsViewModel.kt @@ -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 diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorBottomSheet.kt index 484c71aa3..ed2cf4997 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorBottomSheet.kt @@ -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(), OnListItemClickListener, - PaginationScrollListener.Callback, View.OnClickListener { + PaginationScrollListener.Callback, + View.OnClickListener { private val viewModel by viewModel { parametersOf(requireNotNull(requireArguments().getParcelable(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() { diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorViewModel.kt index 7e671f1e0..4488d0875 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorViewModel.kt @@ -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()) } diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/adapter/ShikiMangaSelectionDecoration.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/adapter/ShikiMangaSelectionDecoration.kt new file mode 100644 index 000000000..72f758bc3 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/adapter/ShikiMangaSelectionDecoration.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt index 6f15f7cd3..c3fdde215 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt @@ -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 ActivityResultLauncher.tryLaunch(input: I, options: ActivityOptionsCo return runCatching { launch(input, options) }.isSuccess -} \ No newline at end of file +} + +fun SharedPreferences.observe() = callbackFlow { + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + trySendBlocking(key) + } + registerOnSharedPreferenceChangeListener(listener) + awaitClose { + unregisterOnSharedPreferenceChangeListener(listener) + } +} + +fun SharedPreferences.observe(key: String, valueProducer: suspend () -> T): Flow = flow { + emit(valueProducer()) + observe().collect { upstreamKey -> + if (upstreamKey == key) { + emit(valueProducer()) + } + } +}.distinctUntilChanged() \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/HttpExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/HttpExt.kt new file mode 100644 index 000000000..058ca4ea6 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/HttpExt.kt @@ -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) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d24c165d3..78a8144e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -291,4 +291,5 @@ Edit category Tracking No favourite categories + Logout \ No newline at end of file diff --git a/app/src/main/res/xml/pref_shikimori.xml b/app/src/main/res/xml/pref_shikimori.xml index b77f72f4f..0e8d9118f 100644 --- a/app/src/main/res/xml/pref_shikimori.xml +++ b/app/src/main/res/xml/pref_shikimori.xml @@ -10,4 +10,10 @@ android:title="@string/loading_" app:iconSpaceReserved="true" /> + +