Auth, search
This commit is contained in:
@@ -1,15 +1,18 @@
|
||||
package org.koitharu.kotatsu.scrobbling.mal.data
|
||||
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONObject
|
||||
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.json.mapJSON
|
||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||
@@ -19,7 +22,8 @@ import org.koitharu.kotatsu.utils.PKCEGenerator
|
||||
private const val REDIRECT_URI = "kotatsu://mal-auth"
|
||||
private const val BASE_OAUTH_URL = "https://myanimelist.net"
|
||||
private const val BASE_API_URL = "https://api.myanimelist.net/v2"
|
||||
private const val MANGA_PAGE_SIZE = 250
|
||||
private const val MANGA_PAGE_SIZE = 10
|
||||
private const val AVATAR_STUB = "https://cdn.myanimelist.net/images/questionmark_50.gif"
|
||||
|
||||
// af16954886b040673378423f5d62cccd
|
||||
|
||||
@@ -29,28 +33,32 @@ class MALRepository(
|
||||
private val db: MangaDatabase,
|
||||
) : ScrobblerRepository {
|
||||
|
||||
private var codeVerifier: String = ""
|
||||
private var codeVerifier: String = getPKCEChallengeCode()
|
||||
|
||||
override val oauthUrl: String
|
||||
get() = "${BASE_OAUTH_URL}/v1/oauth2/authorize?" +
|
||||
get() = "$BASE_OAUTH_URL/v1/oauth2/authorize?" +
|
||||
"response_type=code" +
|
||||
"&client_id=af16954886b040673378423f5d62cccd" +
|
||||
"&redirect_uri=${REDIRECT_URI}" +
|
||||
"&code_challenge=${getPKCEChallengeCode()}" +
|
||||
"&redirect_uri=$REDIRECT_URI" +
|
||||
"&code_challenge=$codeVerifier" +
|
||||
"&code_challenge_method=plain"
|
||||
|
||||
override val isAuthorized: Boolean
|
||||
get() = storage.accessToken != null
|
||||
|
||||
override val cachedUser: ScrobblerUser?
|
||||
get() = TODO("Not yet implemented")
|
||||
get() {
|
||||
return storage.user
|
||||
}
|
||||
|
||||
override suspend fun authorize(code: String?) {
|
||||
val body = FormBody.Builder()
|
||||
if (code != null) {
|
||||
body.add("client_id", "af16954886b040673378423f5d62cccd")
|
||||
body.add("code", code)
|
||||
body.add("code_verifier", getPKCEChallengeCode())
|
||||
body.add("grant_type", "authorization_code")
|
||||
body.add("code", code)
|
||||
body.add("redirect_uri", REDIRECT_URI)
|
||||
body.add("code_verifier", codeVerifier)
|
||||
}
|
||||
val request = Request.Builder()
|
||||
.post(body.build())
|
||||
@@ -64,7 +72,7 @@ class MALRepository(
|
||||
override suspend fun loadUser(): ScrobblerUser {
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url("${BASE_API_URL}/users")
|
||||
.url("${BASE_API_URL}/users/@me")
|
||||
val response = okHttp.newCall(request.build()).await().parseJson()
|
||||
return MALUser(response).also { storage.user = it }
|
||||
}
|
||||
@@ -74,23 +82,74 @@ class MALRepository(
|
||||
}
|
||||
|
||||
override suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> {
|
||||
TODO("Not yet implemented")
|
||||
val pageOffset = offset % MANGA_PAGE_SIZE
|
||||
val url = BASE_API_URL.toHttpUrl().newBuilder()
|
||||
.addPathSegment("manga")
|
||||
.addQueryParameter("offset", (pageOffset + 1).toString())
|
||||
.addQueryParameter("nsfw", "true")
|
||||
.addEncodedQueryParameter("q", query.take(64)) // WARNING! MAL API throws a 400 when the query is over 64 characters
|
||||
.build()
|
||||
val request = Request.Builder().url(url).get().build()
|
||||
val response = okHttp.newCall(request).await().parseJson()
|
||||
val data = response.getJSONArray("data")
|
||||
val mangas = data.mapJSON { jsonToManga(it) }
|
||||
return if (pageOffset != 0) mangas.drop(pageOffset) else mangas
|
||||
}
|
||||
|
||||
override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {
|
||||
TODO("Not yet implemented")
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url("${BASE_API_URL}/manga/$id")
|
||||
val response = okHttp.newCall(request.build()).await().parseJson()
|
||||
return ScrobblerMangaInfo(response)
|
||||
}
|
||||
|
||||
override suspend fun createRate(mangaId: Long, scrobblerMangaId: Long) {
|
||||
TODO("Not yet implemented")
|
||||
val body = FormBody.Builder()
|
||||
.add("status", "reading")
|
||||
.add("score", "0")
|
||||
val url = BASE_API_URL.toHttpUrl().newBuilder()
|
||||
.addPathSegment("manga")
|
||||
.addPathSegment(scrobblerMangaId.toString())
|
||||
.addPathSegment("my_list_status")
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.put(body.build())
|
||||
.build()
|
||||
val response = okHttp.newCall(request).await().parseJson()
|
||||
}
|
||||
|
||||
override suspend fun updateRate(rateId: Int, mangaId: Long, chapter: MangaChapter) {
|
||||
TODO("Not yet implemented")
|
||||
val body = FormBody.Builder()
|
||||
.add("status", "reading")
|
||||
.add("score", "0")
|
||||
val url = BASE_API_URL.toHttpUrl().newBuilder()
|
||||
.addPathSegment("manga")
|
||||
.addPathSegment(mangaId.toString())
|
||||
.addPathSegment("my_list_status")
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.put(body.build())
|
||||
.build()
|
||||
val response = okHttp.newCall(request).await().parseJson()
|
||||
}
|
||||
|
||||
override suspend fun updateRate(rateId: Int, mangaId: Long, rating: Float, status: String?, comment: String?) {
|
||||
TODO("Not yet implemented")
|
||||
val body = FormBody.Builder()
|
||||
.add("status", status!!)
|
||||
.add("score", rating.toString())
|
||||
val url = BASE_API_URL.toHttpUrl().newBuilder()
|
||||
.addPathSegment("manga")
|
||||
.addPathSegment(mangaId.toString())
|
||||
.addPathSegment("my_list_status")
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.put(body.build())
|
||||
.build()
|
||||
val response = okHttp.newCall(request).await().parseJson()
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
@@ -102,11 +161,39 @@ class MALRepository(
|
||||
return codeVerifier
|
||||
}
|
||||
|
||||
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 = ""
|
||||
)
|
||||
}
|
||||
return ScrobblerManga(
|
||||
id = 1,
|
||||
name = "",
|
||||
altName = null,
|
||||
cover = "",
|
||||
url = ""
|
||||
)
|
||||
}
|
||||
|
||||
private fun ScrobblerMangaInfo(json: JSONObject) = ScrobblerMangaInfo(
|
||||
id = json.getLong("id"),
|
||||
name = json.getString("title"),
|
||||
cover = json.getJSONObject("main_picture").getString("large"),
|
||||
url = "",
|
||||
descriptionHtml = json.getString("synopsis"),
|
||||
)
|
||||
|
||||
private fun MALUser(json: JSONObject) = ScrobblerUser(
|
||||
id = json.getLong("id"),
|
||||
nickname = json.getString("nickname"),
|
||||
avatar = json.getString("avatar"),
|
||||
service = ScrobblerService.SHIKIMORI,
|
||||
nickname = json.getString("name"),
|
||||
avatar = json.getString("picture") ?: AVATAR_STUB,
|
||||
service = ScrobblerService.MAL,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -5,17 +5,25 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.CircleCropTransformation
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.utils.PreferenceIconTarget
|
||||
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MALSettingsFragment : BasePreferenceFragment(R.string.mal) {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: MALSettingsViewModel.Factory
|
||||
|
||||
@@ -47,6 +55,11 @@ class MALSettingsFragment : BasePreferenceFragment(R.string.mal) {
|
||||
val pref = findPreference<Preference>(KEY_USER) ?: return
|
||||
pref.isSelectable = user == null
|
||||
pref.title = user?.nickname ?: getString(R.string.sign_in)
|
||||
ImageRequest.Builder(requireContext())
|
||||
.data(user?.avatar)
|
||||
.transformations(CircleCropTransformation())
|
||||
.target(PreferenceIconTarget(pref))
|
||||
.enqueueWith(coil)
|
||||
findPreference<Preference>(KEY_LOGOUT)?.isVisible = user != null
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.koitharu.kotatsu.databinding.ActivitySettingsBinding
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.scrobbling.anilist.ui.AniListSettingsFragment
|
||||
import org.koitharu.kotatsu.scrobbling.mal.ui.MALSettingsFragment
|
||||
import org.koitharu.kotatsu.scrobbling.shikimori.ui.ShikimoriSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment
|
||||
@@ -151,6 +152,9 @@ class SettingsActivity :
|
||||
|
||||
HOST_ANILIST_AUTH ->
|
||||
return AniListSettingsFragment.newInstance(authCode = uri.getQueryParameter("code"))
|
||||
|
||||
HOST_MAL_AUTH ->
|
||||
return MALSettingsFragment.newInstance(authCode = uri.getQueryParameter("code"))
|
||||
}
|
||||
finishAfterTransition()
|
||||
return null
|
||||
@@ -169,6 +173,7 @@ class SettingsActivity :
|
||||
|
||||
private const val HOST_SHIKIMORI_AUTH = "shikimori-auth"
|
||||
private const val HOST_ANILIST_AUTH = "anilist-auth"
|
||||
private const val HOST_MAL_AUTH = "mal-auth"
|
||||
|
||||
fun newIntent(context: Context) = Intent(context, SettingsActivity::class.java)
|
||||
|
||||
|
||||
BIN
app/src/main/res/drawable-hdpi/ic_anilist.png
Normal file
BIN
app/src/main/res/drawable-hdpi/ic_anilist.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 804 B |
BIN
app/src/main/res/drawable-mdpi/ic_anilist.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_anilist.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 517 B |
BIN
app/src/main/res/drawable-xhdpi/ic_anilist.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/ic_anilist.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 906 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_anilist.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_anilist.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_anilist.png
Normal file
BIN
app/src/main/res/drawable-xxxhdpi/ic_anilist.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
|
||||
Reference in New Issue
Block a user