From 8d749df290f0eac70dd8fff9f9d3d09c2e14a442 Mon Sep 17 00:00:00 2001 From: len Date: Wed, 21 Dec 2016 22:39:46 +0100 Subject: [PATCH] Score formatting. Hide API from Anilist/Kitsu services. --- .../tachiyomi/data/track/TrackService.kt | 31 ++- .../tachiyomi/data/track/anilist/Anilist.kt | 242 ++++++++---------- .../data/track/anilist/AnilistApi.kt | 155 +++++++---- .../data/track/anilist/AnilistInterceptor.kt | 2 +- .../data/track/anilist/AnilistModels.kt | 86 +++++++ .../data/track/anilist/{model => }/OAuth.kt | 2 +- .../data/track/anilist/model/ALManga.kt | 17 -- .../data/track/anilist/model/ALUserLists.kt | 6 - .../data/track/anilist/model/ALUserManga.kt | 29 --- .../tachiyomi/data/track/kitsu/Kitsu.kt | 158 +++--------- .../tachiyomi/data/track/kitsu/KitsuApi.kt | 233 ++++++++++++----- .../tachiyomi/data/track/kitsu/KitsuModels.kt | 13 + .../data/track/myanimelist/MyAnimeList.kt | 36 +-- .../tachiyomi/ui/manga/track/TrackFragment.kt | 11 +- .../tachiyomi/ui/manga/track/TrackHolder.kt | 2 +- .../ui/manga/track/TrackPresenter.kt | 4 +- .../main/res/layout/dialog_track_score.xml | 1 + 17 files changed, 573 insertions(+), 455 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt rename app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/{model => }/OAuth.kt (79%) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALManga.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALUserLists.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALUserManga.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt index 4d52ee6983..5e14c66d61 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt @@ -21,6 +21,23 @@ abstract class TrackService(val id: Int) { // Name of the manga sync service to display abstract val name: String + @DrawableRes + abstract fun getLogo(): Int + + abstract fun getLogoColor(): Int + + abstract fun getStatusList(): List + + abstract fun getStatus(status: Int): String + + abstract fun getScoreList(): List + + open fun indexToScore(index: Int): Float { + return index.toFloat() + } + + abstract fun displayScore(track: Track): String + abstract fun login(username: String, password: String): Completable open val isLogged: Boolean @@ -37,20 +54,6 @@ abstract class TrackService(val id: Int) { abstract fun refresh(track: Track): Observable - abstract fun getStatus(status: Int): String - - abstract fun getStatusList(): List - - @DrawableRes - abstract fun getLogo(): Int - - abstract fun getLogoColor(): Int - - // TODO better support (decimals) - abstract fun maxScore(): Int - - abstract fun formatScore(track: Track): String - fun saveCredentials(username: String, password: String) { preferences.setTrackCredentials(this, username, password) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt index 56464aec46..70d909b7f2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt @@ -2,15 +2,12 @@ package eu.kanade.tachiyomi.data.track.anilist import android.content.Context import android.graphics.Color -import com.github.salomonbrys.kotson.int -import com.github.salomonbrys.kotson.string import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.track.TrackService import rx.Completable import rx.Observable -import timber.log.Timber class Anilist(private val context: Context, id: Int) : TrackService(id) { @@ -29,110 +26,12 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { private val interceptor by lazy { AnilistInterceptor(getPassword()) } - private val api by lazy { - AnilistApi.createService(networkService.client.newBuilder() - .addInterceptor(interceptor) - .build()) - } + private val api by lazy { AnilistApi(client, interceptor) } override fun getLogo() = R.drawable.al override fun getLogoColor() = Color.rgb(18, 25, 35) - override fun maxScore() = 100 - - override fun login(username: String, password: String) = login(password) - - fun login(authCode: String): Completable { - // Create a new api with the default client to avoid request interceptions. - return AnilistApi.createService(client) - // Request the access token from the API with the authorization code. - .requestAccessToken(authCode) - // Save the token in the interceptor. - .doOnNext { interceptor.setAuth(it) } - // Obtain the authenticated user from the API. - .zipWith(api.getCurrentUser().map { - preferences.anilistScoreType().set(it["score_type"].int) - it["id"].string - }, { oauth, user -> Pair(user, oauth.refresh_token!!) }) - // Save service credentials (username and refresh token). - .doOnNext { saveCredentials(it.first, it.second) } - // Logout on any error. - .doOnError { logout() } - .toCompletable() - } - - override fun logout() { - super.logout() - interceptor.setAuth(null) - } - - override fun search(query: String): Observable> { - return api.search(query, 1) - .flatMap { Observable.from(it) } - .filter { it.type != "Novel" } - .map { it.toTrack() } - .toList() - } - - fun getList(): Observable> { - return api.getList(getUsername()) - .flatMap { Observable.from(it.flatten()) } - .map { it.toTrack() } - .toList() - } - - override fun add(track: Track): Observable { - return api.addManga(track.remote_id, track.last_chapter_read, track.getAnilistStatus()) - .doOnNext { it.body().close() } - .doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") } - .doOnError { Timber.e(it) } - .map { track } - } - - override fun update(track: Track): Observable { - if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { - track.status = COMPLETED - } - return api.updateManga(track.remote_id, track.last_chapter_read, track.getAnilistStatus(), - track.getAnilistScore()) - .doOnNext { it.body().close() } - .doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") } - .doOnError { Timber.e(it) } - .map { track } - } - - override fun bind(track: Track): Observable { - return getList() - .flatMap { userlist -> - track.sync_id = id - val remoteTrack = userlist.find { it.remote_id == track.remote_id } - if (remoteTrack != null) { - track.copyPersonalFrom(remoteTrack) - update(track) - } else { - // Set default fields if it's not found in the list - track.score = DEFAULT_SCORE.toFloat() - track.status = DEFAULT_STATUS - add(track) - } - } - } - - override fun refresh(track: Track): Observable { - return getList() - .map { myList -> - val remoteTrack = myList.find { it.remote_id == track.remote_id } - if (remoteTrack != null) { - track.copyPersonalFrom(remoteTrack) - track.total_chapters = remoteTrack.total_chapters - track - } else { - throw Exception("Could not find manga") - } - } - } - override fun getStatusList(): List { return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) } @@ -148,43 +47,118 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) { } } - private fun Track.getAnilistStatus() = when (status) { - READING -> "reading" - COMPLETED -> "completed" - ON_HOLD -> "on-hold" - DROPPED -> "dropped" - PLAN_TO_READ -> "plan to read" - else -> throw NotImplementedError("Unknown status") + override fun getScoreList(): List { + return when (preferences.anilistScoreType().getOrDefault()) { + // 10 point + 0 -> IntRange(0, 10).map(Int::toString) + // 100 point + 1 -> IntRange(0, 100).map(Int::toString) + // 5 stars + 2 -> IntRange(0, 5).map { "$it ★" } + // Smiley + 3 -> listOf("-", "😦", "😐", "😊") + // 10 point decimal + 4 -> IntRange(0, 100).map { (it / 10f).toString() } + else -> throw Exception("Unknown score type") + } } - fun Track.getAnilistScore(): String = when (preferences.anilistScoreType().getOrDefault()) { - // 10 point - 0 -> Math.floor(score.toDouble() / 10).toInt().toString() - // 100 point - 1 -> score.toInt().toString() - // 5 stars - 2 -> when { - score == 0f -> "0" - score < 30 -> "1" - score < 50 -> "2" - score < 70 -> "3" - score < 90 -> "4" - else -> "5" + override fun indexToScore(index: Int): Float { + return when (preferences.anilistScoreType().getOrDefault()) { + // 10 point + 0 -> index * 10f + // 100 point + 1 -> index.toFloat() + // 5 stars + 2 -> index * 20f + // Smiley + 3 -> index * 30f + // 10 point decimal + 4 -> index / 10f + else -> throw Exception("Unknown score type") } - // Smiley - 3 -> when { - score == 0f -> "0" - score <= 30 -> ":(" - score <= 60 -> ":|" - else -> ":)" - } - // 10 point decimal - 4 -> (score / 10).toString() - else -> throw Exception("Unknown score type") } - override fun formatScore(track: Track): String { - return track.getAnilistScore() + override fun displayScore(track: Track): String { + val score = track.score + return when (preferences.anilistScoreType().getOrDefault()) { + 2 -> "${(score / 20).toInt()} ★" + 3 -> when { + score == 0f -> "0" + score <= 30 -> "😦" + score <= 60 -> "😐" + else -> "😊" + } + else -> track.toAnilistScore() + } + } + + override fun login(username: String, password: String) = login(password) + + fun login(authCode: String): Completable { + return api.login(authCode) + // Save the token in the interceptor. + .doOnNext { interceptor.setAuth(it) } + // Obtain the authenticated user from the API. + .zipWith(api.getCurrentUser().map { pair -> + preferences.anilistScoreType().set(pair.second) + pair.first + }, { oauth, user -> Pair(user, oauth.refresh_token!!) }) + // Save service credentials (username and refresh token). + .doOnNext { saveCredentials(it.first, it.second) } + // Logout on any error. + .doOnError { logout() } + .toCompletable() + } + + override fun logout() { + super.logout() + interceptor.setAuth(null) + } + + override fun search(query: String): Observable> { + return api.search(query) + } + + override fun add(track: Track): Observable { + return api.addLibManga(track) + } + + override fun update(track: Track): Observable { + if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { + track.status = COMPLETED + } + + return api.updateLibManga(track) + } + + override fun bind(track: Track): Observable { + return api.findLibManga(getUsername(), track) + .flatMap { remoteTrack -> + if (remoteTrack != null) { + track.copyPersonalFrom(remoteTrack) + update(track) + } else { + // Set default fields if it's not found in the list + track.score = DEFAULT_SCORE.toFloat() + track.status = DEFAULT_STATUS + add(track) + } + } + } + + override fun refresh(track: Track): Observable { + // TODO getLibManga method? + return api.findLibManga(getUsername(), track) + .map { remoteTrack -> + if (remoteTrack != null) { + track.copyPersonalFrom(remoteTrack) + track.total_chapters = remoteTrack.total_chapters + track + } else { + throw Exception("Could not find manga") + } + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index 8a48684322..29c4551fca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -1,11 +1,11 @@ package eu.kanade.tachiyomi.data.track.anilist import android.net.Uri +import com.github.salomonbrys.kotson.int +import com.github.salomonbrys.kotson.string import com.google.gson.JsonObject +import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.network.POST -import eu.kanade.tachiyomi.data.track.anilist.model.ALManga -import eu.kanade.tachiyomi.data.track.anilist.model.ALUserLists -import eu.kanade.tachiyomi.data.track.anilist.model.OAuth import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.ResponseBody @@ -16,7 +16,110 @@ import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.* import rx.Observable -interface AnilistApi { +class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { + + private val rest = restBuilder() + .client(client.newBuilder().addInterceptor(interceptor).build()) + .build() + .create(Rest::class.java) + + private fun restBuilder() = Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) + + fun login(authCode: String): Observable { + return restBuilder() + .client(client) + .build() + .create(Rest::class.java) + .requestAccessToken(authCode) + } + + fun getCurrentUser(): Observable> { + return rest.getCurrentUser() + .map { it["id"].string to it["score_type"].int } + } + + fun search(query: String): Observable> { + return rest.search(query, 1) + .map { list -> + list.filter { it.type != "Novel" }.map { it.toTrack() } + } + } + + fun getList(username: String): Observable> { + return rest.getLib(username) + .map { lib -> + lib.flatten().map { it.toTrack() } + } + } + + fun addLibManga(track: Track): Observable { + return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus()) + .doOnNext { it.body().close() } + .doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") } + .map { track } + } + + fun updateLibManga(track: Track): Observable { + return rest.updateLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus(), + track.toAnilistScore()) + .doOnNext { it.body().close() } + .doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") } + .map { track } + } + + fun findLibManga(username: String, track: Track) : Observable { + // TODO avoid getting the entire list + return getList(username) + .map { list -> list.find { it.remote_id == track.remote_id } } + } + + private interface Rest { + + @FormUrlEncoded + @POST("auth/access_token") + fun requestAccessToken( + @Field("code") code: String, + @Field("grant_type") grant_type: String = "authorization_code", + @Field("client_id") client_id: String = clientId, + @Field("client_secret") client_secret: String = clientSecret, + @Field("redirect_uri") redirect_uri: String = clientUrl + ) : Observable + + @GET("user") + fun getCurrentUser(): Observable + + @GET("manga/search/{query}") + fun search( + @Path("query") query: String, + @Query("page") page: Int + ): Observable> + + @GET("user/{username}/mangalist") + fun getLib( + @Path("username") username: String + ): Observable + + @FormUrlEncoded + @PUT("mangalist") + fun addLibManga( + @Field("id") id: Int, + @Field("chapters_read") chapters_read: Int, + @Field("list_status") list_status: String + ) : Observable> + + @FormUrlEncoded + @PUT("mangalist") + fun updateLibManga( + @Field("id") id: Int, + @Field("chapters_read") chapters_read: Int, + @Field("list_status") list_status: String, + @Field("score") score_raw: String + ) : Observable> + + } companion object { private const val clientId = "tachiyomi-hrtje" @@ -39,50 +142,6 @@ interface AnilistApi { .add("refresh_token", token) .build()) - fun createService(client: OkHttpClient) = Retrofit.Builder() - .baseUrl(baseUrl) - .client(client) - .addConverterFactory(GsonConverterFactory.create()) - .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) - .build() - .create(AnilistApi::class.java) - } - @FormUrlEncoded - @POST("auth/access_token") - fun requestAccessToken( - @Field("code") code: String, - @Field("grant_type") grant_type: String = "authorization_code", - @Field("client_id") client_id: String = clientId, - @Field("client_secret") client_secret: String = clientSecret, - @Field("redirect_uri") redirect_uri: String = clientUrl) - : Observable - - @GET("user") - fun getCurrentUser(): Observable - - @GET("manga/search/{query}") - fun search(@Path("query") query: String, @Query("page") page: Int): Observable> - - @GET("user/{username}/mangalist") - fun getList(@Path("username") username: String): Observable - - @FormUrlEncoded - @PUT("mangalist") - fun addManga( - @Field("id") id: Int, - @Field("chapters_read") chapters_read: Int, - @Field("list_status") list_status: String) - : Observable> - - @FormUrlEncoded - @PUT("mangalist") - fun updateManga( - @Field("id") id: Int, - @Field("chapters_read") chapters_read: Int, - @Field("list_status") list_status: String, - @Field("score") score_raw: String) - : Observable> - } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt index 45b81864b4..a25b7fe79a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt @@ -1,7 +1,7 @@ package eu.kanade.tachiyomi.data.track.anilist import com.google.gson.Gson -import eu.kanade.tachiyomi.data.track.anilist.model.OAuth +import eu.kanade.tachiyomi.data.track.anilist.OAuth import okhttp3.Interceptor import okhttp3.Response diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt new file mode 100644 index 0000000000..9e1695c5cb --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt @@ -0,0 +1,86 @@ +package eu.kanade.tachiyomi.data.track.anilist + +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.data.track.TrackManager +import uy.kohesive.injekt.injectLazy + +data class ALManga( + val id: Int, + val title_romaji: String, + val type: String, + val total_chapters: Int) { + + fun toTrack() = Track.create(TrackManager.ANILIST).apply { + remote_id = this@ALManga.id + title = title_romaji + total_chapters = this@ALManga.total_chapters + } +} + +data class ALUserManga( + val id: Int, + val list_status: String, + val score_raw: Int, + val chapters_read: Int, + val manga: ALManga) { + + fun toTrack() = Track.create(TrackManager.ANILIST).apply { + remote_id = manga.id + status = toTrackStatus() + score = score_raw.toFloat() + last_chapter_read = chapters_read + } + + fun toTrackStatus() = when (list_status) { + "reading" -> Anilist.READING + "completed" -> Anilist.COMPLETED + "on-hold" -> Anilist.ON_HOLD + "dropped" -> Anilist.DROPPED + "plan to read" -> Anilist.PLAN_TO_READ + else -> throw NotImplementedError("Unknown status") + } +} + +data class ALUserLists(val lists: Map>) { + + fun flatten() = lists.values.flatten() +} + +fun Track.toAnilistStatus() = when (status) { + Anilist.READING -> "reading" + Anilist.COMPLETED -> "completed" + Anilist.ON_HOLD -> "on-hold" + Anilist.DROPPED -> "dropped" + Anilist.PLAN_TO_READ -> "plan to read" + else -> throw NotImplementedError("Unknown status") +} + +private val preferences: PreferencesHelper by injectLazy() + +fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().getOrDefault()) { + // 10 point + 0 -> Math.floor(score.toDouble() / 10).toInt().toString() + // 100 point + 1 -> score.toInt().toString() + // 5 stars + 2 -> when { + score == 0f -> "0" + score < 30 -> "1" + score < 50 -> "2" + score < 70 -> "3" + score < 90 -> "4" + else -> "5" + } + // Smiley + 3 -> when { + score == 0f -> "0" + score <= 30 -> ":(" + score <= 60 -> ":|" + else -> ":)" + } + // 10 point decimal + 4 -> (score / 10).toString() + else -> throw Exception("Unknown score type") +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/OAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt similarity index 79% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/OAuth.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt index 008e9a9f87..6f5238b377 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/OAuth.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/OAuth.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.data.track.anilist.model +package eu.kanade.tachiyomi.data.track.anilist data class OAuth( val access_token: String, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALManga.kt deleted file mode 100644 index d63ffae003..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALManga.kt +++ /dev/null @@ -1,17 +0,0 @@ -package eu.kanade.tachiyomi.data.track.anilist.model - -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackManager - -data class ALManga( - val id: Int, - val title_romaji: String, - val type: String, - val total_chapters: Int) { - - fun toTrack() = Track.create(TrackManager.ANILIST).apply { - remote_id = this@ALManga.id - title = title_romaji - total_chapters = this@ALManga.total_chapters - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALUserLists.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALUserLists.kt deleted file mode 100644 index 2e1018bcce..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALUserLists.kt +++ /dev/null @@ -1,6 +0,0 @@ -package eu.kanade.tachiyomi.data.track.anilist.model - -data class ALUserLists(val lists: Map>) { - - fun flatten() = lists.values.flatten() -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALUserManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALUserManga.kt deleted file mode 100644 index 6692a8b441..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALUserManga.kt +++ /dev/null @@ -1,29 +0,0 @@ -package eu.kanade.tachiyomi.data.track.anilist.model - -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.data.track.anilist.Anilist - -data class ALUserManga( - val id: Int, - val list_status: String, - val score_raw: Int, - val chapters_read: Int, - val manga: ALManga) { - - fun toTrack() = Track.create(TrackManager.ANILIST).apply { - remote_id = manga.id - status = toTrackStatus() - score = score_raw.toFloat() - last_chapter_read = chapters_read - } - - fun toTrackStatus() = when (list_status) { - "reading" -> Anilist.READING - "completed" -> Anilist.COMPLETED - "on-hold" -> Anilist.ON_HOLD - "dropped" -> Anilist.DROPPED - "plan to read" -> Anilist.PLAN_TO_READ - else -> throw NotImplementedError("Unknown status") - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt index c9de3bac33..807dba005e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt @@ -2,14 +2,12 @@ package eu.kanade.tachiyomi.data.track.kitsu import android.content.Context import android.graphics.Color -import com.github.salomonbrys.kotson.* import com.google.gson.Gson import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.TrackService import rx.Completable import rx.Observable -import timber.log.Timber import uy.kohesive.injekt.injectLazy class Kitsu(private val context: Context, id: Int) : TrackService(id) { @@ -31,10 +29,37 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) { private val interceptor by lazy { KitsuInterceptor(this, gson) } - private val api by lazy { - KitsuApi.createService(client.newBuilder() - .addInterceptor(interceptor) - .build()) + private val api by lazy { KitsuApi(client, interceptor) } + + override fun getLogo(): Int { + return R.drawable.kitsu + } + + override fun getLogoColor(): Int { + return Color.rgb(51, 37, 50) + } + + override fun getStatusList(): List { + return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) + } + + override fun getStatus(status: Int): String = with(context) { + when (status) { + READING -> getString(R.string.reading) + COMPLETED -> getString(R.string.completed) + ON_HOLD -> getString(R.string.on_hold) + DROPPED -> getString(R.string.dropped) + PLAN_TO_READ -> getString(R.string.plan_to_read) + else -> "" + } + } + + override fun getScoreList(): List { + return IntRange(0, 10).map { (it.toFloat() / 2).toString() } + } + + override fun displayScore(track: Track): String { + return track.toKitsuScore() } private fun getUserId(): String { @@ -55,10 +80,9 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) { } override fun login(username: String, password: String): Completable { - return KitsuApi.createLoginService(client) - .requestAccessToken(username, password) + return api.login(username, password) .doOnNext { interceptor.newAuth(it) } - .flatMap { api.getCurrentUser().map { it["data"].array[0]["id"].string } } + .flatMap { api.getCurrentUser() } .doOnNext { userId -> saveCredentials(username, userId) } .doOnError { logout() } .toCompletable() @@ -71,11 +95,6 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) { override fun search(query: String): Observable> { return api.search(query) - .map { json -> - val data = json["data"].array - data.map { KitsuManga(it.obj).toTrack() } - } - .doOnError { Timber.e(it) } } override fun bind(track: Track): Observable { @@ -95,125 +114,26 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) { private fun find(track: Track): Observable { return api.findLibManga(getUserId(), track.remote_id) - .map { json -> - val data = json["data"].array - if (data.size() > 0) { - KitsuLibManga(data[0].obj, json["included"].array[0].obj).toTrack() - } else { - null - } - } } override fun add(track: Track): Observable { - // @formatter:off - val data = jsonObject( - "type" to "libraryEntries", - "attributes" to jsonObject( - "status" to track.getKitsuStatus(), - "progress" to track.last_chapter_read - ), - "relationships" to jsonObject( - "user" to jsonObject( - "data" to jsonObject( - "id" to getUserId(), - "type" to "users" - ) - ), - "media" to jsonObject( - "data" to jsonObject( - "id" to track.remote_id, - "type" to "manga" - ) - ) - ) - ) - // @formatter:on - - return api.addLibManga(jsonObject("data" to data)) - .doOnNext { json -> track.remote_id = json["data"]["id"].int } - .doOnError { Timber.e(it) } - .map { track } + return api.addLibManga(track, getUserId()) } override fun update(track: Track): Observable { if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { track.status = COMPLETED } - // @formatter:off - val data = jsonObject( - "type" to "libraryEntries", - "id" to track.remote_id, - "attributes" to jsonObject( - "status" to track.getKitsuStatus(), - "progress" to track.last_chapter_read, - "rating" to track.getKitsuScore() - ) - ) - // @formatter:on - return api.updateLibManga(track.remote_id, jsonObject("data" to data)) - .map { track } + return api.updateLibManga(track) } override fun refresh(track: Track): Observable { - return api.getLibManga(track.remote_id) - .map { json -> - val data = json["data"].array - if (data.size() > 0) { - val include = json["included"].array[0].obj - val remoteTrack = KitsuLibManga(data[0].obj, include).toTrack() - track.copyPersonalFrom(remoteTrack) - track.total_chapters = remoteTrack.total_chapters - track - } else { - throw Exception("Could not find manga") - } + return api.getLibManga(track) + .doOnNext { remoteTrack -> + track.copyPersonalFrom(remoteTrack) + track.total_chapters = remoteTrack.total_chapters } } - override fun getStatusList(): List { - return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) - } - - override fun getStatus(status: Int): String = with(context) { - when (status) { - READING -> getString(R.string.reading) - COMPLETED -> getString(R.string.completed) - ON_HOLD -> getString(R.string.on_hold) - DROPPED -> getString(R.string.dropped) - PLAN_TO_READ -> getString(R.string.plan_to_read) - else -> "" - } - } - - private fun Track.getKitsuStatus() = when (status) { - READING -> "current" - COMPLETED -> "completed" - ON_HOLD -> "on_hold" - DROPPED -> "dropped" - PLAN_TO_READ -> "planned" - else -> throw Exception("Unknown status") - } - - private fun Track.getKitsuScore(): String { - return if (score > 0) (score / 2).toString() else "" - } - - override fun getLogo(): Int { - return R.drawable.kitsu - } - - override fun getLogoColor(): Int { - return Color.rgb(51, 37, 50) - } - - override fun maxScore(): Int { - return 10 - } - - override fun formatScore(track: Track): String { - return track.getKitsuScore() - } - } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index 8ab8cdbda5..ce5510818d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.data.track.kitsu +import com.github.salomonbrys.kotson.* import com.google.gson.JsonObject +import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.network.POST import okhttp3.FormBody import okhttp3.OkHttpClient @@ -10,7 +12,171 @@ import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.* import rx.Observable -interface KitsuApi { +class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) { + + private val rest = Retrofit.Builder() + .baseUrl(baseUrl) + .client(client.newBuilder().addInterceptor(interceptor).build()) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) + .build() + .create(KitsuApi.Rest::class.java) + + fun login(username: String, password: String): Observable { + return Retrofit.Builder() + .baseUrl(loginUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) + .build() + .create(KitsuApi.LoginRest::class.java) + .requestAccessToken(username, password) + } + + fun getCurrentUser(): Observable { + return rest.getCurrentUser().map { it["data"].array[0]["id"].string } + } + + fun search(query: String): Observable> { + return rest.search(query) + .map { json -> + val data = json["data"].array + data.map { KitsuManga(it.obj).toTrack() } + } + } + + fun findLibManga(userId: String, remoteId: Int): Observable { + return rest.findLibManga(userId, remoteId) + .map { json -> + val data = json["data"].array + if (data.size() > 0) { + KitsuLibManga(data[0].obj, json["included"].array[0].obj).toTrack() + } else { + null + } + } + } + + fun addLibManga(track: Track, userId: String): Observable { + return Observable.defer { + // @formatter:off + val data = jsonObject( + "type" to "libraryEntries", + "attributes" to jsonObject( + "status" to track.toKitsuStatus(), + "progress" to track.last_chapter_read + ), + "relationships" to jsonObject( + "user" to jsonObject( + "data" to jsonObject( + "id" to userId, + "type" to "users" + ) + ), + "media" to jsonObject( + "data" to jsonObject( + "id" to track.remote_id, + "type" to "manga" + ) + ) + ) + ) + // @formatter:on + + rest.addLibManga(jsonObject("data" to data)) + .map { json -> + track.remote_id = json["data"]["id"].int + track + } + } + } + + fun updateLibManga(track: Track): Observable { + return Observable.defer { + // @formatter:off + val data = jsonObject( + "type" to "libraryEntries", + "id" to track.remote_id, + "attributes" to jsonObject( + "status" to track.toKitsuStatus(), + "progress" to track.last_chapter_read, + "rating" to track.toKitsuScore() + ) + ) + // @formatter:on + + rest.updateLibManga(track.remote_id, jsonObject("data" to data)) + .map { track } + } + } + + fun getLibManga(track: Track): Observable { + return rest.getLibManga(track.remote_id) + .map { json -> + val data = json["data"].array + if (data.size() > 0) { + val include = json["included"].array[0].obj + KitsuLibManga(data[0].obj, include).toTrack() + } else { + throw Exception("Could not find manga") + } + } + } + + private interface Rest { + + @GET("users") + fun getCurrentUser( + @Query("filter[self]", encoded = true) self: Boolean = true + ): Observable + + @GET("manga") + fun search( + @Query("filter[text]", encoded = true) query: String + ): Observable + + @GET("library-entries") + fun getLibManga( + @Query("filter[id]", encoded = true) remoteId: Int, + @Query("include") includes: String = "media" + ): Observable + + @GET("library-entries") + fun findLibManga( + @Query("filter[user_id]", encoded = true) userId: String, + @Query("filter[media_id]", encoded = true) remoteId: Int, + @Query("page[limit]", encoded = true) limit: Int = 10000, + @Query("include") includes: String = "media" + ): Observable + + @Headers("Content-Type: application/vnd.api+json") + @POST("library-entries") + fun addLibManga( + @Body data: JsonObject + ): Observable + + @Headers("Content-Type: application/vnd.api+json") + @PATCH("library-entries/{id}") + fun updateLibManga( + @Path("id") remoteId: Int, + @Body data: JsonObject + ): Observable + + } + + private interface LoginRest { + + @FormUrlEncoded + @POST("oauth/token") + fun requestAccessToken( + @Field("username") username: String, + @Field("password") password: String, + @Field("grant_type") grantType: String = "password", + @Field("client_id") client_id: String = clientId, + @Field("client_secret") client_secret: String = clientSecret + ): Observable + + } companion object { private const val clientId = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd" @@ -18,21 +184,6 @@ interface KitsuApi { private const val baseUrl = "https://kitsu.io/api/edge/" private const val loginUrl = "https://kitsu.io/api/" - fun createService(client: OkHttpClient) = Retrofit.Builder() - .baseUrl(baseUrl) - .client(client) - .addConverterFactory(GsonConverterFactory.create()) - .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) - .build() - .create(KitsuApi::class.java) - - fun createLoginService(client: OkHttpClient) = Retrofit.Builder() - .baseUrl(loginUrl) - .client(client) - .addConverterFactory(GsonConverterFactory.create()) - .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) - .build() - .create(KitsuApi::class.java) fun refreshTokenRequest(token: String) = POST("${loginUrl}oauth/token", body = FormBody.Builder() @@ -41,53 +192,7 @@ interface KitsuApi { .add("client_secret", clientSecret) .add("refresh_token", token) .build()) + } - @FormUrlEncoded - @POST("oauth/token") - fun requestAccessToken( - @Field("username") username: String, - @Field("password") password: String, - @Field("grant_type") grantType: String = "password", - @Field("client_id") client_id: String = clientId, - @Field("client_secret") client_secret: String = clientSecret - ) : Observable - - @GET("users") - fun getCurrentUser( - @Query("filter[self]", encoded = true) self: Boolean = true - ) : Observable - - @GET("manga") - fun search( - @Query("filter[text]", encoded = true) query: String - ): Observable - - @GET("library-entries") - fun getLibManga( - @Query("filter[id]", encoded = true) remoteId: Int, - @Query("include") includes: String = "media" - ) : Observable - - @GET("library-entries") - fun findLibManga( - @Query("filter[user_id]", encoded = true) userId: String, - @Query("filter[media_id]", encoded = true) remoteId: Int, - @Query("page[limit]", encoded = true) limit: Int = 10000, - @Query("include") includes: String = "media" - ) : Observable - - @Headers("Content-Type: application/vnd.api+json") - @POST("library-entries") - fun addLibManga( - @Body data: JsonObject - ) : Observable - - @Headers("Content-Type: application/vnd.api+json") - @PATCH("library-entries/{id}") - fun updateLibManga( - @Path("id") remoteId: Int, - @Body data: JsonObject - ) : Observable - -} +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt index 7066749627..42af1640ed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt @@ -42,3 +42,16 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) : KitsuManga(manga) { } } + +fun Track.toKitsuStatus() = when (status) { + Kitsu.READING -> "current" + Kitsu.COMPLETED -> "completed" + Kitsu.ON_HOLD -> "on_hold" + Kitsu.DROPPED -> "dropped" + Kitsu.PLAN_TO_READ -> "planned" + else -> throw Exception("Unknown status") +} + +fun Track.toKitsuScore(): String { + return if (score > 0) (score / 2).toString() else "" +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt index b0e25261c2..9db563151d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt @@ -62,9 +62,26 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { override fun getLogoColor() = Color.rgb(46, 81, 162) - override fun maxScore() = 10 + override fun getStatus(status: Int): String = with(context) { + when (status) { + READING -> getString(R.string.reading) + COMPLETED -> getString(R.string.completed) + ON_HOLD -> getString(R.string.on_hold) + DROPPED -> getString(R.string.dropped) + PLAN_TO_READ -> getString(R.string.plan_to_read) + else -> "" + } + } - override fun formatScore(track: Track): String { + override fun getStatusList(): List { + return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) + } + + override fun getScoreList(): List { + return IntRange(0, 10).map(Int::toString) + } + + override fun displayScore(track: Track): String { return track.score.toInt().toString() } @@ -238,21 +255,6 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { } } - override fun getStatus(status: Int): String = with(context) { - when (status) { - READING -> getString(R.string.reading) - COMPLETED -> getString(R.string.completed) - ON_HOLD -> getString(R.string.on_hold) - DROPPED -> getString(R.string.dropped) - PLAN_TO_READ -> getString(R.string.plan_to_read) - else -> "" - } - } - - override fun getStatusList(): List { - return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) - } - fun createHeaders(username: String, password: String) { val builder = Headers.Builder() builder.add("Authorization", Credentials.basic(username, password)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackFragment.kt index 10e47917a3..fa4feb475b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackFragment.kt @@ -157,9 +157,16 @@ class TrackFragment : BaseRxFragment() { val view = dialog.customView if (view != null) { val np = view.findViewById(R.id.score_picker) as NumberPicker - np.maxValue = item.service.maxScore() + val scores = item.service.getScoreList().toTypedArray() + np.maxValue = scores.size - 1 + np.displayedValues = scores + // Set initial value - np.value = item.track.score.toInt() + val displayedScore = item.service.displayScore(item.track) + if (displayedScore != "-") { + val index = scores.indexOf(displayedScore) + np.value = if (index != -1) index else 0 + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt index 9a9271dd20..9ca33f6925 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt @@ -30,7 +30,7 @@ class TrackHolder(private val view: View, private val fragment: TrackFragment) track_chapters.text = "${track.last_chapter_read}/" + if (track.total_chapters > 0) track.total_chapters else "-" track_status.text = item.service.getStatus(track.status) - track_score.text = if (track.score == 0f) "-" else item.service.formatScore(track) + track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track) } else { track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button) track_title.setText(R.string.action_edit) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt index 6988c299b7..6d799b0c46 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt @@ -122,9 +122,9 @@ class TrackPresenter : BasePresenter() { updateRemote(track, item.service) } - fun setScore(item: TrackItem, score: Int) { + fun setScore(item: TrackItem, index: Int) { val track = item.track!! - track.score = score.toFloat() + track.score = item.service.indexToScore(index) updateRemote(track, item.service) } diff --git a/app/src/main/res/layout/dialog_track_score.xml b/app/src/main/res/layout/dialog_track_score.xml index f671792d18..a1120097d5 100644 --- a/app/src/main/res/layout/dialog_track_score.xml +++ b/app/src/main/res/layout/dialog_track_score.xml @@ -10,6 +10,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" + android:descendantFocusability="blocksDescendants" app:max="10" app:min="0"/>