diff --git a/README.md b/README.md
index ab5cbe31e..acdd1ab05 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ Kotatsu is a free and open source manga reader for Android.
* Tablet-optimized Material You UI
* Standard and Webtoon-optimized reader
* Notifications about new chapters with updates feed
-* Integration with manga tracking services: Shikimori, AniList, MyAnimeList
+* Integration with manga tracking services: Shikimori, AniList, MyAnimeList, Kitsu
* Password/fingerprint protect access to the app
* History and favourites [synchronization](https://github.com/KotatsuApp/kotatsu-syncserver) across devices
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ff15dabe3..8f5089233 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -208,9 +208,21 @@
+
+
+
+
+
+
+
+
= setOf(shikimoriScrobbler, aniListScrobbler, malScrobbler)
+ kitsuScrobbler: KitsuScrobbler
+ ): Set<@JvmSuppressWildcards Scrobbler> = setOf(shikimoriScrobbler, aniListScrobbler, malScrobbler, kitsuScrobbler)
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerService.kt
index 5b47a3a25..28f860d4d 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerService.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerService.kt
@@ -12,5 +12,6 @@ enum class ScrobblerService(
SHIKIMORI(1, R.string.shikimori, R.drawable.ic_shikimori),
ANILIST(2, R.string.anilist, R.drawable.ic_anilist),
- MAL(3, R.string.mal, R.drawable.ic_mal)
+ MAL(3, R.string.mal, R.drawable.ic_mal),
+ KITSU(4, R.string.kitsu, R.drawable.ic_kitsu)
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuAuthenticator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuAuthenticator.kt
new file mode 100644
index 000000000..106651267
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuAuthenticator.kt
@@ -0,0 +1,22 @@
+package org.koitharu.kotatsu.scrobbling.kitsu.data
+
+import okhttp3.Authenticator
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.Route
+import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
+import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
+import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType
+import javax.inject.Inject
+import javax.inject.Provider
+
+class KitsuAuthenticator @Inject constructor(
+ @ScrobblerType(ScrobblerService.KITSU) private val storage: ScrobblerStorage,
+ private val repositoryProvider: Provider,
+) : Authenticator {
+
+ override fun authenticate(route: Route?, response: Response): Request? {
+ TODO("Not yet implemented")
+ }
+
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuInterceptor.kt
new file mode 100644
index 000000000..13b0069e3
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuInterceptor.kt
@@ -0,0 +1,25 @@
+package org.koitharu.kotatsu.scrobbling.kitsu.data
+
+import okhttp3.Interceptor
+import okhttp3.Response
+import org.koitharu.kotatsu.core.network.CommonHeaders
+import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
+
+private const val JSON = "application/json"
+
+class KitsuInterceptor(private val storage: ScrobblerStorage) : Interceptor {
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val sourceRequest = chain.request()
+ val request = sourceRequest.newBuilder()
+ request.header(CommonHeaders.CONTENT_TYPE, JSON)
+ request.header(CommonHeaders.ACCEPT, JSON)
+ if (!sourceRequest.url.pathSegments.contains("oauth")) {
+ storage.accessToken?.let {
+ request.header(CommonHeaders.AUTHORIZATION, "Bearer $it")
+ }
+ }
+ return chain.proceed(request.build())
+ }
+
+}
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
new file mode 100644
index 000000000..74acb0cae
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuRepository.kt
@@ -0,0 +1,92 @@
+package org.koitharu.kotatsu.scrobbling.kitsu.data
+
+import android.content.Context
+import dagger.hilt.android.qualifiers.ApplicationContext
+import okhttp3.FormBody
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.db.MangaDatabase
+import org.koitharu.kotatsu.parsers.model.MangaChapter
+import org.koitharu.kotatsu.parsers.util.await
+import org.koitharu.kotatsu.parsers.util.parseJson
+import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository
+import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
+import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
+import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo
+import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
+import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
+
+private const val BASE_WEB_URL = "https://kitsu.io"
+
+class KitsuRepository(
+ @ApplicationContext context: Context,
+ private val okHttp: OkHttpClient,
+ private val storage: ScrobblerStorage,
+ private val db: MangaDatabase,
+) : ScrobblerRepository {
+
+ private val clientId = context.getString(R.string.kitsu_clientId)
+ private val clientSecret = context.getString(R.string.kitsu_clientSecret)
+
+ override val oauthUrl: String = "kotatsu+kitsu://auth"
+
+ override val isAuthorized: Boolean
+ get() = storage.accessToken != null
+
+ override val cachedUser: ScrobblerUser?
+ get() {
+ return storage.user
+ }
+
+ override suspend fun authorize(code: String?) {
+ val body = FormBody.Builder()
+ if (code != null) {
+ body.add("grant_type", "password")
+ body.add("username", "test@test")
+ body.add("password", "test")
+ } else {
+ body.add("grant_type", "refresh_token")
+ body.add("refresh_token", checkNotNull(storage.refreshToken))
+ }
+ val request = Request.Builder()
+ .post(body.build())
+ .url("${BASE_WEB_URL}/api/oauth/token")
+ val response = okHttp.newCall(request.build()).await().parseJson()
+ storage.accessToken = response.getString("access_token")
+ storage.refreshToken = response.getString("refresh_token")
+ }
+
+ override suspend fun loadUser(): ScrobblerUser {
+ TODO("Not yet implemented")
+ }
+
+ override fun logout() {
+ TODO("Not yet implemented")
+ }
+
+ override suspend fun unregister(mangaId: Long) {
+ return db.scrobblingDao.delete(ScrobblerService.KITSU.id, mangaId)
+ }
+
+ override suspend fun findManga(query: String, offset: Int): List {
+ TODO("Not yet implemented")
+ }
+
+ override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {
+ TODO("Not yet implemented")
+ }
+
+ override suspend fun createRate(mangaId: Long, scrobblerMangaId: Long) {
+ TODO("Not yet implemented")
+ }
+
+ override suspend fun updateRate(rateId: Int, mangaId: Long, chapter: MangaChapter) {
+ TODO("Not yet implemented")
+ }
+
+ override suspend fun updateRate(rateId: Int, mangaId: Long, rating: Float, status: String?, comment: String?) {
+ TODO("Not yet implemented")
+ }
+
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/domain/KitsuScrobbler.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/domain/KitsuScrobbler.kt
new file mode 100644
index 000000000..8321dae01
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/domain/KitsuScrobbler.kt
@@ -0,0 +1,40 @@
+package org.koitharu.kotatsu.scrobbling.kitsu.domain
+
+import org.koitharu.kotatsu.core.db.MangaDatabase
+import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
+import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
+import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
+import org.koitharu.kotatsu.scrobbling.kitsu.data.KitsuRepository
+import javax.inject.Inject
+
+class KitsuScrobbler @Inject constructor(
+ private val repository: KitsuRepository,
+ db: MangaDatabase,
+) : Scrobbler(db, ScrobblerService.KITSU, repository) {
+
+ init {
+ statuses[ScrobblingStatus.PLANNED] = "planned"
+ statuses[ScrobblingStatus.READING] = "current"
+ statuses[ScrobblingStatus.COMPLETED] = "completed"
+ statuses[ScrobblingStatus.ON_HOLD] = "on_hold"
+ statuses[ScrobblingStatus.DROPPED] = "dropped"
+ }
+
+ override suspend fun updateScrobblingInfo(
+ mangaId: Long,
+ rating: Float,
+ status: ScrobblingStatus?,
+ comment: String?
+ ) {
+ val entity = db.scrobblingDao.find(scrobblerService.id, mangaId)
+ requireNotNull(entity) { "Scrobbling info for manga $mangaId not found" }
+ repository.updateRate(
+ rateId = entity.id,
+ mangaId = entity.mangaId,
+ rating = rating,
+ status = statuses[status],
+ comment = comment,
+ )
+ }
+
+}
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
new file mode 100644
index 000000000..55ae9d8ff
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/ui/KitsuAuthActivity.kt
@@ -0,0 +1,32 @@
+package org.koitharu.kotatsu.scrobbling.kitsu.ui
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.core.graphics.Insets
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.base.ui.BaseActivity
+import org.koitharu.kotatsu.databinding.ActivityKitsuAuthBinding
+
+class KitsuAuthActivity : BaseActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(ActivityKitsuAuthBinding.inflate(layoutInflater))
+ }
+
+ override fun onWindowInsetsChanged(insets: Insets) {
+ val basePadding = resources.getDimensionPixelOffset(R.dimen.screen_padding)
+ binding.root.setPadding(
+ basePadding + insets.left,
+ basePadding + insets.top,
+ basePadding + insets.right,
+ basePadding + insets.bottom,
+ )
+ }
+
+ companion object {
+ fun newIntent(context: Context) = Intent(context, KitsuAuthActivity::class.java)
+ }
+
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt
index 74e04a2a2..820cfb23a 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt
@@ -19,8 +19,10 @@ import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
+import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity
+import org.koitharu.kotatsu.scrobbling.kitsu.data.KitsuRepository
import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
import org.koitharu.kotatsu.sync.domain.SyncController
@@ -41,6 +43,9 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
@Inject
lateinit var malRepository: MALRepository
+ @Inject
+ lateinit var kitsuRepository: KitsuRepository
+
@Inject
lateinit var syncController: SyncController
@@ -64,6 +69,7 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
bindScrobblerSummary(AppSettings.KEY_SHIKIMORI, shikimoriRepository)
bindScrobblerSummary(AppSettings.KEY_ANILIST, aniListRepository)
bindScrobblerSummary(AppSettings.KEY_MAL, malRepository)
+ bindScrobblerSummary(AppSettings.KEY_KITSU, kitsuRepository)
bindSyncSummary()
}
@@ -103,6 +109,15 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
true
}
+ AppSettings.KEY_KITSU -> {
+ if (!kitsuRepository.isAuthorized) {
+ launchScrobblerAuth(kitsuRepository)
+ } else {
+ startActivity(ScrobblerConfigActivity.newIntent(preference.context, ScrobblerService.KITSU))
+ }
+ true
+ }
+
AppSettings.KEY_SYNC -> {
val am = AccountManager.get(requireContext())
val accountType = getString(R.string.account_type_sync)
@@ -125,7 +140,7 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
private fun bindScrobblerSummary(
key: String,
- repository: org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository
+ repository: ScrobblerRepository
) {
val pref = findPreference(key) ?: return
if (!repository.isAuthorized) {
@@ -151,7 +166,7 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
}
}
- private fun launchScrobblerAuth(repository: org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository) {
+ private fun launchScrobblerAuth(repository: ScrobblerRepository) {
runCatching {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(repository.oauthUrl)
diff --git a/app/src/main/res/drawable/ic_kitsu.xml b/app/src/main/res/drawable/ic_kitsu.xml
new file mode 100644
index 000000000..b124346cb
--- /dev/null
+++ b/app/src/main/res/drawable/ic_kitsu.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_kitsu_auth.xml b/app/src/main/res/layout/activity_kitsu_auth.xml
new file mode 100644
index 000000000..f6166696e
--- /dev/null
+++ b/app/src/main/res/layout/activity_kitsu_auth.xml
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml
index ab014ce7c..a213a621e 100644
--- a/app/src/main/res/values/constants.xml
+++ b/app/src/main/res/values/constants.xml
@@ -13,6 +13,8 @@
9887
wrMqFosItQWsmB8dtAHfIFPDt15FfQi2ZGiKkJoW
6cd8e6349e9a36bc1fc1ab97703c9fd1
+ dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd
+ 54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151
SxhkCVnqVLbGogvi
xPDACTLHnHU9Nfjv
org.koitharu.kotatsu.history
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ac8e8fa69..9f9d16dea 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -420,6 +420,8 @@
Port
Proxy
Invalid value
+ Kitsu
+ Enter your email and password to continue
%1$s (%2$s)
Downloaded
Images optimization proxy
diff --git a/app/src/main/res/xml/pref_services.xml b/app/src/main/res/xml/pref_services.xml
index bb35834b0..2cd394f50 100644
--- a/app/src/main/res/xml/pref_services.xml
+++ b/app/src/main/res/xml/pref_services.xml
@@ -30,21 +30,26 @@
-
-
+
+
+
+