From c576b62d51fdc02c4752f4edad6e4628bff37d18 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 22 Jun 2025 10:25:48 +0300 Subject: [PATCH] Scrobbling improvements and fixes (closes #1448) --- .../kotatsu/details/ui/DetailsActivity.kt | 6 +- .../ui/scrobbling/ScrobblingInfoSheet.kt | 2 +- .../anilist/data/AniListRepository.kt | 12 +- .../common/domain/model/ScrobblerManga.kt | 4 +- .../common/domain/model/ScrobblingInfo.kt | 7 + .../ui/selector/ScrobblingSelectorSheet.kt | 2 +- .../ui/selector/adapter/ScrobblingMangaAD.kt | 3 + .../scrobbling/kitsu/data/KitsuRepository.kt | 3 + .../scrobbling/kitsu/ui/KitsuAuthActivity.kt | 64 ++++-- .../scrobbling/mal/data/MALRepository.kt | 25 ++- .../shikimori/data/ShikimoriRepository.kt | 6 +- app/src/main/res/drawable/ic_star_small.xml | 11 + .../main/res/layout/activity_kitsu_auth.xml | 189 +++++++++--------- .../main/res/layout/item_scrobbling_info.xml | 1 + .../res/layout/sheet_scrobbling_selector.xml | 13 +- app/src/main/res/values-night/colors.xml | 1 + app/src/main/res/values/colors.xml | 1 + 17 files changed, 213 insertions(+), 137 deletions(-) create mode 100644 app/src/main/res/drawable/ic_star_small.xml diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index bc22dade1..69d76e435 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -248,8 +248,10 @@ class DetailsActivity : } R.id.button_scrobbling_more -> { - val manga = viewModel.getMangaOrNull() ?: return - router.showScrobblingSelectorSheet(manga, null) + router.showScrobblingSelectorSheet( + manga = viewModel.getMangaOrNull() ?: return, + scrobblerService = viewModel.scrobblingInfo.value.firstOrNull()?.scrobbler + ) } R.id.button_related_more -> { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoSheet.kt index ef497c3a7..a79add7fd 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoSheet.kt @@ -153,7 +153,7 @@ class ScrobblingInfoSheet : R.id.action_edit -> { val manga = viewModel.manga.value ?: return false val scrobblerService = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.scrobbler - router.showScrobblingSelectorSheet(manga, scrobblerService) + activity?.router?.showScrobblingSelectorSheet(manga, scrobblerService) dismiss() } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListRepository.kt index 5fa6ce203..d8415554b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListRepository.kt @@ -133,7 +133,7 @@ class AniListRepository @Inject constructor( """, ) val data = response.getJSONObject("data").getJSONObject("Page").getJSONArray("media") - return data.mapJSON { ScrobblerManga(it) } + return data.mapJSON { ScrobblerManga(it, query) } } override suspend fun createRate(mangaId: Long, scrobblerMangaId: Long) { @@ -225,7 +225,7 @@ class AniListRepository @Inject constructor( db.getScrobblingDao().upsert(entity) } - private fun ScrobblerManga(json: JSONObject): ScrobblerManga { + private fun ScrobblerManga(json: JSONObject, sourceTitle: String): ScrobblerManga { val title = json.getJSONObject("title") return ScrobblerManga( id = json.getLong("id"), @@ -233,6 +233,14 @@ class AniListRepository @Inject constructor( altName = title.getStringOrNull("native"), cover = json.getJSONObject("coverImage").getString("medium"), url = json.getString("siteUrl"), + isBestMatch = sourceTitle.let { + title.keys().forEach { key -> + if (title.getStringOrNull(key)?.equals(it, ignoreCase = true) == true) { + return@let true + } + } + false + }, ) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerManga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerManga.kt index f4d3be9bc..608ac1e1d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerManga.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerManga.kt @@ -6,9 +6,11 @@ data class ScrobblerManga( val id: Long, val name: String, val altName: String?, - val cover: String, + val cover: String?, val url: String, + val isBestMatch: Boolean, ) : ListModel { + override fun areItemsTheSame(other: ListModel): Boolean { return other is ScrobblerManga && other.id == id } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblingInfo.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblingInfo.kt index ff106007f..e9f7fe47f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblingInfo.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblingInfo.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.scrobbling.common.domain.model +import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.model.ListModel data class ScrobblingInfo( @@ -19,4 +20,10 @@ data class ScrobblingInfo( override fun areItemsTheSame(other: ListModel): Boolean { return other is ScrobblingInfo && other.scrobbler == scrobbler } + + override fun getChangePayload(previousState: ListModel): Any? = when { + previousState !is ScrobblingInfo -> null + previousState.status != status || previousState.rating != rating -> ListModelDiffCallback.PAYLOAD_ANYTHING_CHANGED + else -> super.getChangePayload(previousState) + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorSheet.kt index b47002d7f..5e8163aef 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorSheet.kt @@ -94,7 +94,7 @@ class ScrobblingSelectorSheet : if (isLoading) { binding.buttonDone.setProgressIcon() } else { - binding.buttonDone.icon = null + binding.buttonDone.setIconResource(R.drawable.ic_check) } binding.tabs.setTabsEnabled(!isLoading) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblingMangaAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblingMangaAD.kt index c864d7ff1..f7f579299 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblingMangaAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblingMangaAD.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ItemMangaListBinding @@ -18,6 +19,8 @@ fun scrobblingMangaAD( bind { binding.textViewTitle.text = item.name + val endIcon = if (item.isBestMatch) R.drawable.ic_star_small else 0 + binding.textViewTitle.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, endIcon, 0) binding.textViewSubtitle.textAndVisible = item.altName binding.imageViewCover.setImageAsync(item.cover, null) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuRepository.kt index a82e10783..93a3a5f3e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuRepository.kt @@ -106,6 +106,9 @@ class KitsuRepository( altName = titles.drop(1).joinToString(), cover = attrs.getJSONObject("posterImage").getStringOrNull("small").orEmpty(), url = "$BASE_WEB_URL/manga/${attrs.getString("slug")}", + isBestMatch = titles.any { + it.equals(query, ignoreCase = true) + } ) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/ui/KitsuAuthActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/ui/KitsuAuthActivity.kt index 4acc0bdcf..73e0402f4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/ui/KitsuAuthActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/ui/KitsuAuthActivity.kt @@ -4,18 +4,25 @@ import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle import android.text.Editable +import android.view.KeyEvent import android.view.View +import android.view.ViewGroup.MarginLayoutParams +import android.widget.TextView import androidx.core.net.toUri import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.util.DefaultTextWatcher -import org.koitharu.kotatsu.core.util.ext.consumeAllSystemBarsInsets -import org.koitharu.kotatsu.core.util.ext.systemBarsInsets +import org.koitharu.kotatsu.core.util.ext.consume import org.koitharu.kotatsu.databinding.ActivityKitsuAuthBinding import org.koitharu.kotatsu.parsers.util.urlEncoded -class KitsuAuthActivity : BaseActivity(), View.OnClickListener, DefaultTextWatcher { +class KitsuAuthActivity : BaseActivity(), + View.OnClickListener, + DefaultTextWatcher, + TextView.OnEditorActionListener { private val regexEmail = Regex("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", RegexOption.IGNORE_CASE) @@ -25,22 +32,33 @@ class KitsuAuthActivity : BaseActivity(), View.OnClick viewBinding.buttonCancel.setOnClickListener(this) viewBinding.buttonDone.setOnClickListener(this) viewBinding.editEmail.addTextChangedListener(this) + viewBinding.editEmail.setOnEditorActionListener(this) viewBinding.editPassword.addTextChangedListener(this) + viewBinding.editPassword.setOnEditorActionListener(this) } override fun onApplyWindowInsets( v: View, insets: WindowInsetsCompat ): WindowInsetsCompat { - val barsInsets = insets.systemBarsInsets - val basePadding = resources.getDimensionPixelOffset(R.dimen.screen_padding) - viewBinding.root.setPadding( - barsInsets.left + basePadding, - barsInsets.top + basePadding, - barsInsets.right + basePadding, - barsInsets.bottom + basePadding, - ) - return insets.consumeAllSystemBarsInsets() + val typeMask = WindowInsetsCompat.Type.systemBars() + val screenPadding = v.resources.getDimensionPixelOffset(R.dimen.screen_padding) + val barsInsets = insets.getInsets(typeMask) + viewBinding.root.updatePadding(top = barsInsets.top) + viewBinding.dockedToolbarChild.updateLayoutParams { + leftMargin = barsInsets.left + rightMargin = barsInsets.right + bottomMargin = barsInsets.bottom + } + viewBinding.layoutEmail.updateLayoutParams { + leftMargin = barsInsets.left + screenPadding + rightMargin = barsInsets.right + screenPadding + } + viewBinding.layoutPassword.updateLayoutParams { + leftMargin = barsInsets.left + screenPadding + rightMargin = barsInsets.right + screenPadding + } + return insets.consume(v, typeMask) } override fun onClick(v: View) { @@ -50,6 +68,28 @@ class KitsuAuthActivity : BaseActivity(), View.OnClick } } + override fun onEditorAction( + v: TextView, + actionId: Int, + event: KeyEvent? + ): Boolean = when (v.id) { + R.id.edit_email -> { + viewBinding.editPassword.requestFocus() + true + } + + R.id.edit_password -> { + if (viewBinding.buttonDone.isEnabled) { + continueAuth() + true + } else { + false + } + } + + else -> false + } + override fun afterTextChanged(s: Editable?) { val email = viewBinding.editEmail.text?.toString()?.trim() val password = viewBinding.editPassword.text?.toString()?.trim() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt index 5abbad1c1..91d21a714 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt @@ -99,7 +99,7 @@ class MALRepository @Inject constructor( val response = okHttp.newCall(request).await().parseJson() check(response.has("data")) { "Invalid response: \"$response\"" } val data = response.getJSONArray("data") - return data.mapJSONNotNull { jsonToManga(it) } + return data.mapJSONNotNull { jsonToManga(it, query) } } override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo { @@ -183,18 +183,17 @@ class MALRepository @Inject constructor( storage.clear() } - private fun jsonToManga(json: JSONObject): ScrobblerManga? { - for (i in 0 until json.length()) { - val node = json.getJSONObject("node") - return ScrobblerManga( - id = node.getLong("id"), - name = node.getString("title"), - altName = null, - cover = node.getJSONObject("main_picture").getString("large"), - url = "$BASE_WEB_URL/manga/${node.getLong("id")}", - ) - } - return null + private fun jsonToManga(json: JSONObject, sourceTitle: String): ScrobblerManga? { + val node = json.getJSONObject("node") + val title = node.getString("title") + return ScrobblerManga( + id = node.getLong("id"), + name = title, + altName = null, + cover = node.optJSONObject("main_picture")?.getStringOrNull("large"), + url = "$BASE_WEB_URL/manga/${node.getLong("id")}", + isBestMatch = title.equals(sourceTitle, ignoreCase = true), + ) } private fun ScrobblerMangaInfo(json: JSONObject) = ScrobblerMangaInfo( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt index 0ca1054ea..afc7e0363 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt @@ -104,7 +104,7 @@ class ShikimoriRepository @Inject constructor( .build() val request = Request.Builder().url(url).get().build() val response = okHttp.newCall(request).await().parseJsonArray() - val list = response.mapJSON { ScrobblerManga(it) } + val list = response.mapJSON { ScrobblerManga(it, query) } return if (pageOffset != 0) list.drop(pageOffset) else list } @@ -195,12 +195,14 @@ class ShikimoriRepository @Inject constructor( db.getScrobblingDao().upsert(entity) } - private fun ScrobblerManga(json: JSONObject) = ScrobblerManga( + private fun ScrobblerManga(json: JSONObject, sourceTitle: String) = ScrobblerManga( id = json.getLong("id"), name = json.getString("name"), altName = json.getStringOrNull("russian"), cover = json.getJSONObject("image").getString("preview").toAbsoluteUrl(DOMAIN), url = json.getString("url").toAbsoluteUrl(DOMAIN), + isBestMatch = sourceTitle.equals(json.getString("name"), ignoreCase = true) + || json.getStringOrNull("russian")?.equals(sourceTitle, ignoreCase = true) == true ) private fun ScrobblerMangaInfo(json: JSONObject) = ScrobblerMangaInfo( diff --git a/app/src/main/res/drawable/ic_star_small.xml b/app/src/main/res/drawable/ic_star_small.xml new file mode 100644 index 000000000..91a98c69e --- /dev/null +++ b/app/src/main/res/drawable/ic_star_small.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/layout/activity_kitsu_auth.xml b/app/src/main/res/layout/activity_kitsu_auth.xml index b3e197a29..714a61db2 100644 --- a/app/src/main/res/layout/activity_kitsu_auth.xml +++ b/app/src/main/res/layout/activity_kitsu_auth.xml @@ -5,15 +5,14 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:orientation="vertical" - android:padding="@dimen/screen_padding"> + android:orientation="vertical"> - + android:layout_height="wrap_content" + android:layout_marginTop="12dp" + android:gravity="center_horizontal" + android:text="@string/email_password_enter_hint" + android:textAppearance="?textAppearanceSubtitle1" /> - - - - - - - - - - - - - - -