Score formatting. Hide API from Anilist/Kitsu services.
This commit is contained in:
parent
091c0c0c71
commit
8d749df290
17 changed files with 573 additions and 455 deletions
|
@ -21,6 +21,23 @@ abstract class TrackService(val id: Int) {
|
||||||
// Name of the manga sync service to display
|
// Name of the manga sync service to display
|
||||||
abstract val name: String
|
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
|
abstract fun login(username: String, password: String): Completable
|
||||||
|
|
||||||
open val isLogged: Boolean
|
open val isLogged: Boolean
|
||||||
|
@ -37,20 +54,6 @@ abstract class TrackService(val id: Int) {
|
||||||
|
|
||||||
abstract fun refresh(track: Track): Observable<Track>
|
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) {
|
fun saveCredentials(username: String, password: String) {
|
||||||
preferences.setTrackCredentials(this, username, password)
|
preferences.setTrackCredentials(this, username, password)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,12 @@ package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
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.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
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 interceptor by lazy { AnilistInterceptor(getPassword()) }
|
||||||
|
|
||||||
private val api by lazy {
|
private val api by lazy { AnilistApi(client, interceptor) }
|
||||||
AnilistApi.createService(networkService.client.newBuilder()
|
|
||||||
.addInterceptor(interceptor)
|
|
||||||
.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLogo() = R.drawable.al
|
override fun getLogo() = R.drawable.al
|
||||||
|
|
||||||
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
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<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()
|
|
||||||
}
|
|
||||||
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
|
|
||||||
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 }
|
|
||||||
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<Track> {
|
|
||||||
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<Int> {
|
override fun getStatusList(): List<Int> {
|
||||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
|
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) {
|
override fun getScoreList(): List<String> {
|
||||||
READING -> "reading"
|
return when (preferences.anilistScoreType().getOrDefault()) {
|
||||||
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
|
// 10 point
|
||||||
0 -> Math.floor(score.toDouble() / 10).toInt().toString()
|
0 -> IntRange(0, 10).map(Int::toString)
|
||||||
// 100 point
|
// 100 point
|
||||||
1 -> score.toInt().toString()
|
1 -> IntRange(0, 100).map(Int::toString)
|
||||||
// 5 stars
|
// 5 stars
|
||||||
2 -> when {
|
2 -> IntRange(0, 5).map { "$it ★" }
|
||||||
score == 0f -> "0"
|
|
||||||
score < 30 -> "1"
|
|
||||||
score < 50 -> "2"
|
|
||||||
score < 70 -> "3"
|
|
||||||
score < 90 -> "4"
|
|
||||||
else -> "5"
|
|
||||||
}
|
|
||||||
// Smiley
|
// Smiley
|
||||||
3 -> when {
|
3 -> listOf("-", "😦", "😐", "😊")
|
||||||
score == 0f -> "0"
|
|
||||||
score <= 30 -> ":("
|
|
||||||
score <= 60 -> ":|"
|
|
||||||
else -> ":)"
|
|
||||||
}
|
|
||||||
// 10 point decimal
|
// 10 point decimal
|
||||||
4 -> (score / 10).toString()
|
4 -> IntRange(0, 100).map { (it / 10f).toString() }
|
||||||
else -> throw Exception("Unknown score type")
|
else -> throw Exception("Unknown score type")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun formatScore(track: Track): String {
|
override fun indexToScore(index: Int): Float {
|
||||||
return track.getAnilistScore()
|
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 {
|
||||||
|
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<List<Track>> {
|
||||||
|
return api.search(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(track: Track): Observable<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.updateLibManga(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(track: Track): Observable<Track> {
|
||||||
|
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<Track> {
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package eu.kanade.tachiyomi.data.track.anilist
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import com.github.salomonbrys.kotson.int
|
||||||
|
import com.github.salomonbrys.kotson.string
|
||||||
import com.google.gson.JsonObject
|
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.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.FormBody
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
|
@ -16,7 +16,110 @@ import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import retrofit2.http.*
|
import retrofit2.http.*
|
||||||
import rx.Observable
|
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 {
|
companion object {
|
||||||
private const val clientId = "tachiyomi-hrtje"
|
private const val clientId = "tachiyomi-hrtje"
|
||||||
|
@ -39,50 +142,6 @@ interface AnilistApi {
|
||||||
.add("refresh_token", token)
|
.add("refresh_token", token)
|
||||||
.build())
|
.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,7 +1,7 @@
|
||||||
package eu.kanade.tachiyomi.data.track.anilist
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
import com.google.gson.Gson
|
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.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
|
||||||
|
|
|
@ -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<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,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.data.track.anilist.model
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
data class OAuth(
|
data class OAuth(
|
||||||
val access_token: String,
|
val access_token: String,
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,14 +2,12 @@ package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import com.github.salomonbrys.kotson.*
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
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 interceptor by lazy { KitsuInterceptor(this, gson) }
|
||||||
|
|
||||||
private val api by lazy {
|
private val api by lazy { KitsuApi(client, interceptor) }
|
||||||
KitsuApi.createService(client.newBuilder()
|
|
||||||
.addInterceptor(interceptor)
|
override fun getLogo(): Int {
|
||||||
.build())
|
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 {
|
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 {
|
override fun login(username: String, password: String): Completable {
|
||||||
return KitsuApi.createLoginService(client)
|
return api.login(username, password)
|
||||||
.requestAccessToken(username, password)
|
|
||||||
.doOnNext { interceptor.newAuth(it) }
|
.doOnNext { interceptor.newAuth(it) }
|
||||||
.flatMap { api.getCurrentUser().map { it["data"].array[0]["id"].string } }
|
.flatMap { api.getCurrentUser() }
|
||||||
.doOnNext { userId -> saveCredentials(username, userId) }
|
.doOnNext { userId -> saveCredentials(username, userId) }
|
||||||
.doOnError { logout() }
|
.doOnError { logout() }
|
||||||
.toCompletable()
|
.toCompletable()
|
||||||
|
@ -71,11 +95,6 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
override fun search(query: String): Observable<List<Track>> {
|
override fun search(query: String): Observable<List<Track>> {
|
||||||
return api.search(query)
|
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> {
|
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?> {
|
private fun find(track: Track): Observable<Track?> {
|
||||||
return api.findLibManga(getUserId(), track.remote_id)
|
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> {
|
override fun add(track: Track): Observable<Track> {
|
||||||
// @formatter:off
|
return api.addLibManga(track, getUserId())
|
||||||
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 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun update(track: Track): Observable<Track> {
|
override fun update(track: Track): Observable<Track> {
|
||||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
track.status = COMPLETED
|
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))
|
return api.updateLibManga(track)
|
||||||
.map { track }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refresh(track: Track): Observable<Track> {
|
override fun refresh(track: Track): Observable<Track> {
|
||||||
return api.getLibManga(track.remote_id)
|
return api.getLibManga(track)
|
||||||
.map { json ->
|
.doOnNext { remoteTrack ->
|
||||||
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.copyPersonalFrom(remoteTrack)
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
track
|
|
||||||
} else {
|
|
||||||
throw Exception("Could not find manga")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
package eu.kanade.tachiyomi.data.track.kitsu
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.*
|
||||||
import com.google.gson.JsonObject
|
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.network.POST
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
@ -10,48 +12,118 @@ import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import retrofit2.http.*
|
import retrofit2.http.*
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
interface KitsuApi {
|
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
|
||||||
|
|
||||||
companion object {
|
private val rest = Retrofit.Builder()
|
||||||
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)
|
.baseUrl(baseUrl)
|
||||||
.client(client)
|
.client(client.newBuilder().addInterceptor(interceptor).build())
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||||
.build()
|
.build()
|
||||||
.create(KitsuApi::class.java)
|
.create(KitsuApi.Rest::class.java)
|
||||||
|
|
||||||
fun createLoginService(client: OkHttpClient) = Retrofit.Builder()
|
fun login(username: String, password: String): Observable<OAuth> {
|
||||||
|
return Retrofit.Builder()
|
||||||
.baseUrl(loginUrl)
|
.baseUrl(loginUrl)
|
||||||
.client(client)
|
.client(client)
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||||
.build()
|
.build()
|
||||||
.create(KitsuApi::class.java)
|
.create(KitsuApi.LoginRest::class.java)
|
||||||
|
.requestAccessToken(username, password)
|
||||||
fun refreshTokenRequest(token: String) = POST("${loginUrl}oauth/token",
|
|
||||||
body = FormBody.Builder()
|
|
||||||
.add("grant_type", "refresh_token")
|
|
||||||
.add("client_id", clientId)
|
|
||||||
.add("client_secret", clientSecret)
|
|
||||||
.add("refresh_token", token)
|
|
||||||
.build())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormUrlEncoded
|
fun getCurrentUser(): Observable<String> {
|
||||||
@POST("oauth/token")
|
return rest.getCurrentUser().map { it["data"].array[0]["id"].string }
|
||||||
fun requestAccessToken(
|
}
|
||||||
@Field("username") username: String,
|
|
||||||
@Field("password") password: String,
|
fun search(query: String): Observable<List<Track>> {
|
||||||
@Field("grant_type") grantType: String = "password",
|
return rest.search(query)
|
||||||
@Field("client_id") client_id: String = clientId,
|
.map { json ->
|
||||||
@Field("client_secret") client_secret: String = clientSecret
|
val data = json["data"].array
|
||||||
) : Observable<OAuth>
|
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")
|
@GET("users")
|
||||||
fun getCurrentUser(
|
fun getCurrentUser(
|
||||||
|
@ -91,3 +163,36 @@ interface KitsuApi {
|
||||||
): Observable<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()
|
||||||
|
.add("grant_type", "refresh_token")
|
||||||
|
.add("client_id", clientId)
|
||||||
|
.add("client_secret", clientSecret)
|
||||||
|
.add("refresh_token", token)
|
||||||
|
.build())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 ""
|
||||||
|
}
|
||||||
|
|
|
@ -62,9 +62,26 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
override fun getLogoColor() = Color.rgb(46, 81, 162)
|
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<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 displayScore(track: Track): String {
|
||||||
return track.score.toInt().toString()
|
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) {
|
fun createHeaders(username: String, password: String) {
|
||||||
val builder = Headers.Builder()
|
val builder = Headers.Builder()
|
||||||
builder.add("Authorization", Credentials.basic(username, password))
|
builder.add("Authorization", Credentials.basic(username, password))
|
||||||
|
|
|
@ -157,9 +157,16 @@ class TrackFragment : BaseRxFragment<TrackPresenter>() {
|
||||||
val view = dialog.customView
|
val view = dialog.customView
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
val np = view.findViewById(R.id.score_picker) as NumberPicker
|
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
|
// 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ class TrackHolder(private val view: View, private val fragment: TrackFragment)
|
||||||
track_chapters.text = "${track.last_chapter_read}/" +
|
track_chapters.text = "${track.last_chapter_read}/" +
|
||||||
if (track.total_chapters > 0) track.total_chapters else "-"
|
if (track.total_chapters > 0) track.total_chapters else "-"
|
||||||
track_status.text = item.service.getStatus(track.status)
|
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 {
|
} else {
|
||||||
track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button)
|
track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button)
|
||||||
track_title.setText(R.string.action_edit)
|
track_title.setText(R.string.action_edit)
|
||||||
|
|
|
@ -122,9 +122,9 @@ class TrackPresenter : BasePresenter<TrackFragment>() {
|
||||||
updateRemote(track, item.service)
|
updateRemote(track, item.service)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setScore(item: TrackItem, score: Int) {
|
fun setScore(item: TrackItem, index: Int) {
|
||||||
val track = item.track!!
|
val track = item.track!!
|
||||||
track.score = score.toFloat()
|
track.score = item.service.indexToScore(index)
|
||||||
updateRemote(track, item.service)
|
updateRemote(track, item.service)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
|
android:descendantFocusability="blocksDescendants"
|
||||||
app:max="10"
|
app:max="10"
|
||||||
app:min="0"/>
|
app:min="0"/>
|
||||||
|
|
||||||
|
|
Reference in a new issue