Scrobbling improvements and fixes (closes #1448)

This commit is contained in:
Koitharu
2025-06-22 10:25:48 +03:00
parent 722c4466bf
commit c576b62d51
17 changed files with 213 additions and 137 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<ActivityKitsuAuthBinding>(), View.OnClickListener, DefaultTextWatcher {
class KitsuAuthActivity : BaseActivity<ActivityKitsuAuthBinding>(),
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<ActivityKitsuAuthBinding>(), 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<MarginLayoutParams> {
leftMargin = barsInsets.left
rightMargin = barsInsets.right
bottomMargin = barsInsets.bottom
}
viewBinding.layoutEmail.updateLayoutParams<MarginLayoutParams> {
leftMargin = barsInsets.left + screenPadding
rightMargin = barsInsets.right + screenPadding
}
viewBinding.layoutPassword.updateLayoutParams<MarginLayoutParams> {
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<ActivityKitsuAuthBinding>(), 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()

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:tint="@color/common_yellow"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M14.43,10l-2.43,-8l-2.43,8l-7.57,0l6.18,4.41l-2.35,7.59l6.17,-4.69l6.18,4.69l-2.35,-7.59l6.17,-4.41z" />
</vector>

View File

@@ -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">
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:drawablePadding="16dp"
android:drawablePadding="@dimen/screen_padding"
android:gravity="center_horizontal"
android:text="@string/kitsu"
android:textAppearance="?textAppearanceHeadline5"
@@ -23,108 +22,98 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<RelativeLayout
<TextView
android:id="@+id/textView_subtitle"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center_horizontal"
android:text="@string/email_password_enter_hint"
android:textAppearance="?textAppearanceSubtitle1" />
<TextView
android:id="@+id/textView_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="12dp"
android:gravity="center_horizontal"
android:text="@string/email_password_enter_hint"
android:textAppearance="?textAppearanceSubtitle1" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_email"
style="?textInputOutlinedStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_below="@id/textView_subtitle"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="30dp"
app:errorIconDrawable="@null"
app:hintEnabled="false">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="emailAddress"
android:hint="@string/email"
android:imeOptions="actionDone"
android:inputType="textEmailAddress"
android:maxLength="512"
android:singleLine="true"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_password"
style="?textInputOutlinedStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_below="@id/layout_email"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="8dp"
app:endIconMode="password_toggle"
app:errorIconDrawable="@null"
app:hintEnabled="false">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="password"
android:hint="@string/password"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:maxLength="512"
android:singleLine="true"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/button_cancel"
style="?materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"
android:text="@android:string/cancel" />
<Button
android:id="@+id/button_done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:enabled="false"
android:text="@string/done"
tools:ignore="RelativeOverlap" />
</RelativeLayout>
<FrameLayout
android:id="@+id/layout_progress"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_email"
style="?textInputOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/screen_padding"
android:layout_marginTop="30dp"
app:errorIconDrawable="@null"
app:hintEnabled="false">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:layout_width="wrap_content"
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" />
android:autofillHints="emailAddress"
android:hint="@string/email"
android:imeOptions="actionNext"
android:inputType="textEmailAddress"
android:maxLength="512"
android:singleLine="true"
android:textSize="16sp" />
</FrameLayout>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_password"
style="?textInputOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/screen_padding"
android:layout_marginTop="8dp"
app:endIconMode="password_toggle"
app:errorIconDrawable="@null"
app:hintEnabled="false">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="password"
android:hint="@string/password"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:maxLength="512"
android:singleLine="true"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="invisible" />
<com.google.android.material.dockedtoolbar.DockedToolbarLayout
android:id="@+id/docked_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="false">
<FrameLayout
android:id="@+id/docked_toolbar_child"
android:layout_width="match_parent"
android:layout_height="@dimen/m3_comp_toolbar_docked_container_height">
<Button
android:id="@+id/button_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
android:text="@android:string/cancel" />
<Button
android:id="@+id/button_done"
style="?materialButtonTonalStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:enabled="false"
android:text="@string/_continue" />
</FrameLayout>
</com.google.android.material.dockedtoolbar.DockedToolbarLayout>
</LinearLayout>

View File

@@ -60,6 +60,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:singleLine="true"
android:textAppearance="?textAppearanceBody1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_icon"

View File

@@ -16,7 +16,9 @@
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
app:contentInsetStart="0dp"
tools:menu="@menu/opt_search">
<RelativeLayout
android:layout_width="match_parent"
@@ -27,20 +29,25 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/button_done"
android:background="@android:color/transparent"
android:clipToPadding="false"
android:paddingStart="@dimen/list_spacing"
android:scrollIndicators="start|end"
app:tabGravity="start"
app:tabMode="scrollable"
tools:ignore="UnusedAttribute" />
tools:ignore="RtlSymmetry,UnusedAttribute" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_done"
style="?materialIconButtonFilledStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginHorizontal="@dimen/toolbar_button_margin"
android:text="@string/done" />
app:icon="@drawable/ic_check" />
</RelativeLayout>

View File

@@ -8,6 +8,7 @@
<color name="warning">#FB8C00</color>
<color name="launcher_background">#222222</color>
<color name="common_green">#81C784</color>
<color name="common_yellow">#FFF176</color>
<color name="common_red">#E57373</color>
<color name="dim2">#C8000000</color>
<color name="nsfw_18">#BF360C</color>

View File

@@ -20,6 +20,7 @@
<color name="launcher_background">#FFFFFF</color>
<color name="common_green">#388E3C</color>
<color name="common_red">#D32F2F</color>
<color name="common_yellow">#FBC02D</color>
<color name="nsfw_18">#FF8A65</color>
<color name="nsfw_16">#FFD54F</color>