Auth, search
This commit is contained in:
@@ -1,15 +1,18 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.mal.data
|
package org.koitharu.kotatsu.scrobbling.mal.data
|
||||||
|
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.util.await
|
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.parsers.util.parseJson
|
||||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository
|
||||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
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.ScrobblerManga
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
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 REDIRECT_URI = "kotatsu://mal-auth"
|
||||||
private const val BASE_OAUTH_URL = "https://myanimelist.net"
|
private const val BASE_OAUTH_URL = "https://myanimelist.net"
|
||||||
private const val BASE_API_URL = "https://api.myanimelist.net/v2"
|
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
|
// af16954886b040673378423f5d62cccd
|
||||||
|
|
||||||
@@ -29,28 +33,32 @@ class MALRepository(
|
|||||||
private val db: MangaDatabase,
|
private val db: MangaDatabase,
|
||||||
) : ScrobblerRepository {
|
) : ScrobblerRepository {
|
||||||
|
|
||||||
private var codeVerifier: String = ""
|
private var codeVerifier: String = getPKCEChallengeCode()
|
||||||
|
|
||||||
override val oauthUrl: String
|
override val oauthUrl: String
|
||||||
get() = "${BASE_OAUTH_URL}/v1/oauth2/authorize?" +
|
get() = "$BASE_OAUTH_URL/v1/oauth2/authorize?" +
|
||||||
"response_type=code" +
|
"response_type=code" +
|
||||||
"&client_id=af16954886b040673378423f5d62cccd" +
|
"&client_id=af16954886b040673378423f5d62cccd" +
|
||||||
"&redirect_uri=${REDIRECT_URI}" +
|
"&redirect_uri=$REDIRECT_URI" +
|
||||||
"&code_challenge=${getPKCEChallengeCode()}" +
|
"&code_challenge=$codeVerifier" +
|
||||||
"&code_challenge_method=plain"
|
"&code_challenge_method=plain"
|
||||||
|
|
||||||
override val isAuthorized: Boolean
|
override val isAuthorized: Boolean
|
||||||
get() = storage.accessToken != null
|
get() = storage.accessToken != null
|
||||||
|
|
||||||
override val cachedUser: ScrobblerUser?
|
override val cachedUser: ScrobblerUser?
|
||||||
get() = TODO("Not yet implemented")
|
get() {
|
||||||
|
return storage.user
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun authorize(code: String?) {
|
override suspend fun authorize(code: String?) {
|
||||||
val body = FormBody.Builder()
|
val body = FormBody.Builder()
|
||||||
if (code != null) {
|
if (code != null) {
|
||||||
body.add("client_id", "af16954886b040673378423f5d62cccd")
|
body.add("client_id", "af16954886b040673378423f5d62cccd")
|
||||||
body.add("code", code)
|
|
||||||
body.add("code_verifier", getPKCEChallengeCode())
|
|
||||||
body.add("grant_type", "authorization_code")
|
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()
|
val request = Request.Builder()
|
||||||
.post(body.build())
|
.post(body.build())
|
||||||
@@ -64,7 +72,7 @@ class MALRepository(
|
|||||||
override suspend fun loadUser(): ScrobblerUser {
|
override suspend fun loadUser(): ScrobblerUser {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.get()
|
.get()
|
||||||
.url("${BASE_API_URL}/users")
|
.url("${BASE_API_URL}/users/@me")
|
||||||
val response = okHttp.newCall(request.build()).await().parseJson()
|
val response = okHttp.newCall(request.build()).await().parseJson()
|
||||||
return MALUser(response).also { storage.user = it }
|
return MALUser(response).also { storage.user = it }
|
||||||
}
|
}
|
||||||
@@ -74,23 +82,74 @@ class MALRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> {
|
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 {
|
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) {
|
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) {
|
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?) {
|
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() {
|
override fun logout() {
|
||||||
@@ -102,11 +161,39 @@ class MALRepository(
|
|||||||
return codeVerifier
|
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(
|
private fun MALUser(json: JSONObject) = ScrobblerUser(
|
||||||
id = json.getLong("id"),
|
id = json.getLong("id"),
|
||||||
nickname = json.getString("nickname"),
|
nickname = json.getString("name"),
|
||||||
avatar = json.getString("avatar"),
|
avatar = json.getString("picture") ?: AVATAR_STUB,
|
||||||
service = ScrobblerService.SHIKIMORI,
|
service = ScrobblerService.MAL,
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,25 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import coil.transform.CircleCropTransformation
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
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.assistedViewModels
|
||||||
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MALSettingsFragment : BasePreferenceFragment(R.string.mal) {
|
class MALSettingsFragment : BasePreferenceFragment(R.string.mal) {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var coil: ImageLoader
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var viewModelFactory: MALSettingsViewModel.Factory
|
lateinit var viewModelFactory: MALSettingsViewModel.Factory
|
||||||
|
|
||||||
@@ -47,6 +55,11 @@ class MALSettingsFragment : BasePreferenceFragment(R.string.mal) {
|
|||||||
val pref = findPreference<Preference>(KEY_USER) ?: return
|
val pref = findPreference<Preference>(KEY_USER) ?: return
|
||||||
pref.isSelectable = user == null
|
pref.isSelectable = user == null
|
||||||
pref.title = user?.nickname ?: getString(R.string.sign_in)
|
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
|
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.main.ui.owners.AppBarOwner
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.scrobbling.anilist.ui.AniListSettingsFragment
|
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.scrobbling.shikimori.ui.ShikimoriSettingsFragment
|
||||||
import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment
|
import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment
|
||||||
import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment
|
import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment
|
||||||
@@ -151,6 +152,9 @@ class SettingsActivity :
|
|||||||
|
|
||||||
HOST_ANILIST_AUTH ->
|
HOST_ANILIST_AUTH ->
|
||||||
return AniListSettingsFragment.newInstance(authCode = uri.getQueryParameter("code"))
|
return AniListSettingsFragment.newInstance(authCode = uri.getQueryParameter("code"))
|
||||||
|
|
||||||
|
HOST_MAL_AUTH ->
|
||||||
|
return MALSettingsFragment.newInstance(authCode = uri.getQueryParameter("code"))
|
||||||
}
|
}
|
||||||
finishAfterTransition()
|
finishAfterTransition()
|
||||||
return null
|
return null
|
||||||
@@ -169,6 +173,7 @@ class SettingsActivity :
|
|||||||
|
|
||||||
private const val HOST_SHIKIMORI_AUTH = "shikimori-auth"
|
private const val HOST_SHIKIMORI_AUTH = "shikimori-auth"
|
||||||
private const val HOST_ANILIST_AUTH = "anilist-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)
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<PreferenceScreen
|
<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
|
<SwitchPreferenceCompat
|
||||||
android:defaultValue="true"
|
android:defaultValue="true"
|
||||||
|
|||||||
Reference in New Issue
Block a user