Browse Source

Score formatting. Hide API from Anilist/Kitsu services.

len 8 years ago
parent
commit
8d749df290

+ 17 - 14
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<Int>
+
+    abstract fun getStatus(status: Int): String
+
+    abstract fun getScoreList(): List<String>
+
+    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<Track>
 
-    abstract fun getStatus(status: Int): String
-
-    abstract fun getStatusList(): List<Int>
-
-    @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)
     }

+ 74 - 100
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,31 +26,83 @@ 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 getStatusList(): List<Int> {
+        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<String> {
+        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")
+        }
+    }
+
+    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")
+        }
+    }
+
+    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 {
-        // 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)
+        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 {
-                    preferences.anilistScoreType().set(it["score_type"].int)
-                    it["id"].string
+                .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) }
@@ -68,45 +117,24 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
     }
 
     override fun search(query: String): Observable<List<Track>> {
-        return api.search(query, 1)
-                .flatMap { Observable.from(it) }
-                .filter { it.type != "Novel" }
-                .map { it.toTrack() }
-                .toList()
-    }
-
-    fun getList(): Observable<List<Track>> {
-        return api.getList(getUsername())
-                .flatMap { Observable.from(it.flatten()) }
-                .map { it.toTrack() }
-                .toList()
+        return api.search(query)
     }
 
     override fun add(track: Track): Observable<Track> {
-        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 }
+        return api.addLibManga(track)
     }
 
     override fun update(track: Track): Observable<Track> {
         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 }
+
+        return api.updateLibManga(track)
     }
 
     override fun bind(track: Track): Observable<Track> {
-        return getList()
-                .flatMap { userlist ->
-                    track.sync_id = id
-                    val remoteTrack = userlist.find { it.remote_id == track.remote_id }
+        return api.findLibManga(getUsername(), track)
+                .flatMap { remoteTrack ->
                     if (remoteTrack != null) {
                         track.copyPersonalFrom(remoteTrack)
                         update(track)
@@ -120,9 +148,9 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
     }
 
     override fun refresh(track: Track): Observable<Track> {
-        return getList()
-                .map { myList ->
-                    val remoteTrack = myList.find { it.remote_id == track.remote_id }
+        // TODO getLibManga method?
+        return api.findLibManga(getUsername(), track)
+                .map { remoteTrack ->
                     if (remoteTrack != null) {
                         track.copyPersonalFrom(remoteTrack)
                         track.total_chapters = remoteTrack.total_chapters
@@ -133,59 +161,5 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
                 }
     }
 
-    override fun getStatusList(): List<Int> {
-        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.getAnilistStatus() = when (status) {
-        READING -> "reading"
-        COMPLETED -> "completed"
-        ON_HOLD -> "on-hold"
-        DROPPED -> "dropped"
-        PLAN_TO_READ -> "plan to read"
-        else -> throw NotImplementedError("Unknown status")
-    }
-
-    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"
-        }
-        // 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()
-    }
-
 }
 

+ 107 - 48
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<OAuth> {
+        return restBuilder()
+                .client(client)
+                .build()
+                .create(Rest::class.java)
+                .requestAccessToken(authCode)
+    }
+
+    fun getCurrentUser(): Observable<Pair<String, Int>> {
+        return rest.getCurrentUser()
+                .map { it["id"].string to it["score_type"].int }
+    }
+
+    fun search(query: String): Observable<List<Track>> {
+        return rest.search(query, 1)
+                .map { list ->
+                    list.filter { it.type != "Novel" }.map { it.toTrack() }
+                }
+    }
+
+    fun getList(username: String): Observable<List<Track>> {
+        return rest.getLib(username)
+                .map { lib ->
+                    lib.flatten().map { it.toTrack() }
+                }
+    }
+
+    fun addLibManga(track: Track): Observable<Track> {
+        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<Track> {
+        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<Track?> {
+        // 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<OAuth>
+
+        @GET("user")
+        fun getCurrentUser(): Observable<JsonObject>
+
+        @GET("manga/search/{query}")
+        fun search(
+                @Path("query") query: String,
+                @Query("page") page: Int
+        ): Observable<List<ALManga>>
+
+        @GET("user/{username}/mangalist")
+        fun getLib(
+                @Path("username") username: String
+        ): Observable<ALUserLists>
+
+        @FormUrlEncoded
+        @PUT("mangalist")
+        fun addLibManga(
+                @Field("id") id: Int,
+                @Field("chapters_read") chapters_read: Int,
+                @Field("list_status") list_status: String
+        ) : Observable<Response<ResponseBody>>
+
+        @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<Response<ResponseBody>>
+
+    }
 
     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<OAuth>
-
-    @GET("user")
-    fun getCurrentUser(): Observable<JsonObject>
-
-    @GET("manga/search/{query}")
-    fun search(@Path("query") query: String, @Query("page") page: Int): Observable<List<ALManga>>
-
-    @GET("user/{username}/mangalist")
-    fun getList(@Path("username") username: String): Observable<ALUserLists>
-
-    @FormUrlEncoded
-    @PUT("mangalist")
-    fun addManga(
-            @Field("id") id: Int,
-            @Field("chapters_read") chapters_read: Int,
-            @Field("list_status") list_status: String)
-            : Observable<Response<ResponseBody>>
-
-    @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<Response<ResponseBody>>
-
 }

+ 1 - 1
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
 

+ 86 - 0
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 = [email protected]
+        title = title_romaji
+        total_chapters = [email protected]_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<String, List<ALUserManga>>) {
+
+    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")
+}

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/OAuth.kt → 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,

+ 0 - 17
app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALManga.kt

@@ -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 = [email protected]
-        title = title_romaji
-        total_chapters = [email protected]_chapters
-    }
-}

+ 0 - 6
app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALUserLists.kt

@@ -1,6 +0,0 @@
-package eu.kanade.tachiyomi.data.track.anilist.model
-
-data class ALUserLists(val lists: Map<String, List<ALUserManga>>) {
-
-    fun flatten() = lists.values.flatten()
-}

+ 0 - 29
app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALUserManga.kt

@@ -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")
-    }
-}

+ 40 - 120
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<Int> {
+        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<String> {
+        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<List<Track>> {
         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<Track> {
@@ -95,125 +114,26 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
 
     private fun find(track: Track): Observable<Track?> {
         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<Track> {
-        // @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<Track> {
         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<Track> {
-        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<Int> {
-        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()
-    }
-
 }

+ 169 - 64
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,29 +12,178 @@ import retrofit2.converter.gson.GsonConverterFactory
 import retrofit2.http.*
 import rx.Observable
 
-interface KitsuApi {
+class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
 
-    companion object {
-        private const val clientId = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
-        private const val clientSecret = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
-        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)
+    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 createLoginService(client: OkHttpClient) = Retrofit.Builder()
+    fun login(username: String, password: String): Observable<OAuth> {
+        return Retrofit.Builder()
                 .baseUrl(loginUrl)
                 .client(client)
                 .addConverterFactory(GsonConverterFactory.create())
                 .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                 .build()
-                .create(KitsuApi::class.java)
+                .create(KitsuApi.LoginRest::class.java)
+                .requestAccessToken(username, password)
+    }
+
+    fun getCurrentUser(): Observable<String> {
+        return rest.getCurrentUser().map { it["data"].array[0]["id"].string }
+    }
+
+    fun search(query: String): Observable<List<Track>> {
+        return rest.search(query)
+                .map { json ->
+                    val data = json["data"].array
+                    data.map { KitsuManga(it.obj).toTrack() }
+                }
+    }
+
+    fun findLibManga(userId: String, remoteId: Int): Observable<Track?> {
+        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<Track> {
+        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<Track> {
+        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<Track> {
+        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<JsonObject>
+
+        @GET("manga")
+        fun search(
+                @Query("filter[text]", encoded = true) query: String
+        ): Observable<JsonObject>
+
+        @GET("library-entries")
+        fun getLibManga(
+                @Query("filter[id]", encoded = true) remoteId: Int,
+                @Query("include") includes: String = "media"
+        ): Observable<JsonObject>
+
+        @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<JsonObject>
+
+        @Headers("Content-Type: application/vnd.api+json")
+        @POST("library-entries")
+        fun addLibManga(
+                @Body data: JsonObject
+        ): Observable<JsonObject>
+
+        @Headers("Content-Type: application/vnd.api+json")
+        @PATCH("library-entries/{id}")
+        fun updateLibManga(
+                @Path("id") remoteId: Int,
+                @Body data: JsonObject
+        ): Observable<JsonObject>
+
+    }
+
+    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<OAuth>
+
+    }
+
+    companion object {
+        private const val clientId = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
+        private const val clientSecret = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
+        private const val baseUrl = "https://kitsu.io/api/edge/"
+        private const val loginUrl = "https://kitsu.io/api/"
+
 
         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<OAuth>
-
-    @GET("users")
-    fun getCurrentUser(
-            @Query("filter[self]", encoded = true) self: Boolean = true
-    ) : Observable<JsonObject>
-
-    @GET("manga")
-    fun search(
-            @Query("filter[text]", encoded = true) query: String
-    ): Observable<JsonObject>
-
-    @GET("library-entries")
-    fun getLibManga(
-            @Query("filter[id]", encoded = true) remoteId: Int,
-            @Query("include") includes: String = "media"
-    ) : Observable<JsonObject>
-
-    @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<JsonObject>
-
-    @Headers("Content-Type: application/vnd.api+json")
-    @POST("library-entries")
-    fun addLibManga(
-            @Body data: JsonObject
-    ) : Observable<JsonObject>
-
-    @Headers("Content-Type: application/vnd.api+json")
-    @PATCH("library-entries/{id}")
-    fun updateLibManga(
-            @Path("id") remoteId: Int,
-            @Body data: JsonObject
-    ) : Observable<JsonObject>
-
-}
+}

+ 13 - 0
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 ""
+}

+ 19 - 17
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 getStatusList(): List<Int> {
+        return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
+    }
+
+    override fun getScoreList(): List<String> {
+        return IntRange(0, 10).map(Int::toString)
+    }
 
-    override fun formatScore(track: Track): String {
+    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<Int> {
-        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))

+ 9 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackFragment.kt

@@ -157,9 +157,16 @@ class TrackFragment : BaseRxFragment<TrackPresenter>() {
         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
+            }
         }
     }
 

+ 1 - 1
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)

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt

@@ -122,9 +122,9 @@ class TrackPresenter : BasePresenter<TrackFragment>() {
         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)
     }
 

+ 1 - 0
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"/>