diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt index af423d496..d8d7be97b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt @@ -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 { - 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, ) } diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/ui/MALSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/ui/MALSettingsFragment.kt index ce5377169..ec0b185cd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/ui/MALSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/ui/MALSettingsFragment.kt @@ -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(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(KEY_LOGOUT)?.isVisible = user != null } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt index e6f8880aa..21bea36ed 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt @@ -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) diff --git a/app/src/main/res/drawable-hdpi/ic_anilist.png b/app/src/main/res/drawable-hdpi/ic_anilist.png new file mode 100644 index 000000000..fdb33acef Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_anilist.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_anilist.png b/app/src/main/res/drawable-mdpi/ic_anilist.png new file mode 100644 index 000000000..4ed58b1e1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_anilist.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_anilist.png b/app/src/main/res/drawable-xhdpi/ic_anilist.png new file mode 100644 index 000000000..6a9d6fa4d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_anilist.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_anilist.png b/app/src/main/res/drawable-xxhdpi/ic_anilist.png new file mode 100644 index 000000000..fb392692c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_anilist.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_anilist.png b/app/src/main/res/drawable-xxxhdpi/ic_anilist.png new file mode 100644 index 000000000..d714c8370 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_anilist.png differ diff --git a/app/src/main/res/drawable/ic_anilist.xml b/app/src/main/res/drawable/ic_anilist.xml deleted file mode 100644 index e9fa65813..000000000 --- a/app/src/main/res/drawable/ic_anilist.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/xml/pref_history.xml b/app/src/main/res/xml/pref_history.xml index 2c3406f19..b0975f4c5 100644 --- a/app/src/main/res/xml/pref_history.xml +++ b/app/src/main/res/xml/pref_history.xml @@ -1,6 +1,7 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto">