Experimental Anilist and Kitsu support (#586)
* Tracking tab with anilist support * Rename MangaSync to Track * Rename variables and methods to track * Kitsu implementation * Variables refactoring * Travis fix?pull/591/head
parent
e3d430eb5e
commit
94ee4e7fb5
@ -1,46 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
||||
|
||||
interface MangaSyncQueries : DbProvider {
|
||||
|
||||
fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get()
|
||||
.`object`(MangaSync::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaSyncTable.TABLE)
|
||||
.where("${MangaSyncTable.COL_MANGA_ID} = ? AND " +
|
||||
"${MangaSyncTable.COL_SYNC_ID} = ?")
|
||||
.whereArgs(manga.id, sync.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getMangasSync(manga: Manga) = db.get()
|
||||
.listOfObjects(MangaSync::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaSyncTable.TABLE)
|
||||
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare()
|
||||
|
||||
fun insertMangasSync(mangas: List<MangaSync>) = db.put().objects(mangas).prepare()
|
||||
|
||||
fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
|
||||
|
||||
fun deleteMangaSyncForManga(manga: Manga) = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(MangaSyncTable.TABLE)
|
||||
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
|
||||
interface TrackQueries : DbProvider {
|
||||
|
||||
fun getTracks(manga: Manga) = db.get()
|
||||
.listOfObjects(Track::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(TrackTable.TABLE)
|
||||
.where("${TrackTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun insertTrack(track: Track) = db.put().`object`(track).prepare()
|
||||
|
||||
fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare()
|
||||
|
||||
fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(TrackTable.TABLE)
|
||||
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
|
||||
.whereArgs(manga.id, sync.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables
|
||||
|
||||
object MangaSyncTable {
|
||||
object TrackTable {
|
||||
|
||||
const val TABLE = "manga_sync"
|
||||
|
@ -1,23 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.data.mangasync.myanimelist.MyAnimeList
|
||||
|
||||
class MangaSyncManager(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
const val MYANIMELIST = 1
|
||||
const val ANILIST = 2
|
||||
}
|
||||
|
||||
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
||||
|
||||
val aniList = Anilist(context, ANILIST)
|
||||
|
||||
// TODO enable anilist
|
||||
val services = listOf(myAnimeList)
|
||||
|
||||
fun getService(id: Int) = services.find { it.id == id }
|
||||
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync
|
||||
|
||||
import android.content.Context
|
||||
import android.support.annotation.CallSuper
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import okhttp3.OkHttpClient
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class MangaSyncService(private val context: Context, val id: Int) {
|
||||
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
val networkService: NetworkHelper by injectLazy()
|
||||
|
||||
open val client: OkHttpClient
|
||||
get() = networkService.client
|
||||
|
||||
// Name of the manga sync service to display
|
||||
abstract val name: String
|
||||
|
||||
abstract fun login(username: String, password: String): Completable
|
||||
|
||||
open val isLogged: Boolean
|
||||
get() = !getUsername().isEmpty() &&
|
||||
!getPassword().isEmpty()
|
||||
|
||||
abstract fun add(manga: MangaSync): Observable<MangaSync>
|
||||
|
||||
abstract fun update(manga: MangaSync): Observable<MangaSync>
|
||||
|
||||
abstract fun bind(manga: MangaSync): Observable<MangaSync>
|
||||
|
||||
abstract fun getStatus(status: Int): String
|
||||
|
||||
fun saveCredentials(username: String, password: String) {
|
||||
preferences.setMangaSyncCredentials(this, username, password)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
open fun logout() {
|
||||
preferences.setMangaSyncCredentials(this, "", "")
|
||||
}
|
||||
|
||||
fun getUsername() = preferences.mangaSyncUsername(this)
|
||||
|
||||
fun getPassword() = preferences.mangaSyncPassword(this)
|
||||
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.anilist
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
|
||||
class Anilist(private val context: Context, id: Int) : MangaSyncService(context, id) {
|
||||
|
||||
companion object {
|
||||
const val READING = 1
|
||||
const val COMPLETED = 2
|
||||
const val ON_HOLD = 3
|
||||
const val DROPPED = 4
|
||||
const val PLAN_TO_READ = 5
|
||||
|
||||
const val DEFAULT_STATUS = READING
|
||||
const val DEFAULT_SCORE = 0
|
||||
}
|
||||
|
||||
override val name = "AniList"
|
||||
|
||||
private val interceptor by lazy { AnilistInterceptor(getPassword()) }
|
||||
|
||||
private val api by lazy {
|
||||
AnilistApi.createService(networkService.client.newBuilder()
|
||||
.addInterceptor(interceptor)
|
||||
.build())
|
||||
}
|
||||
|
||||
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 { it["id"].toString() })
|
||||
{ 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)
|
||||
}
|
||||
|
||||
fun search(query: String): Observable<List<MangaSync>> {
|
||||
return api.search(query, 1)
|
||||
.flatMap { Observable.from(it) }
|
||||
.filter { it.type != "Novel" }
|
||||
.map { it.toMangaSync() }
|
||||
.toList()
|
||||
}
|
||||
|
||||
fun getList(): Observable<List<MangaSync>> {
|
||||
return api.getList(getUsername())
|
||||
.flatMap { Observable.from(it.flatten()) }
|
||||
.map { it.toMangaSync() }
|
||||
.toList()
|
||||
}
|
||||
|
||||
override fun add(manga: MangaSync): Observable<MangaSync> {
|
||||
return api.addManga(manga.remote_id, manga.last_chapter_read, manga.getAnilistStatus(),
|
||||
manga.score.toInt())
|
||||
.doOnNext { it.body().close() }
|
||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
|
||||
.doOnError { Timber.e(it, it.message) }
|
||||
.map { manga }
|
||||
}
|
||||
|
||||
override fun update(manga: MangaSync): Observable<MangaSync> {
|
||||
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
|
||||
manga.status = COMPLETED
|
||||
}
|
||||
return api.updateManga(manga.remote_id, manga.last_chapter_read, manga.getAnilistStatus(),
|
||||
manga.score.toInt())
|
||||
.doOnNext { it.body().close() }
|
||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
|
||||
.doOnError { Timber.e(it, it.message) }
|
||||
.map { manga }
|
||||
}
|
||||
|
||||
override fun bind(manga: MangaSync): Observable<MangaSync> {
|
||||
return getList()
|
||||
.flatMap { userlist ->
|
||||
manga.sync_id = id
|
||||
val mangaFromList = userlist.find { it.remote_id == manga.remote_id }
|
||||
if (mangaFromList != null) {
|
||||
manga.copyPersonalFrom(mangaFromList)
|
||||
update(manga)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
manga.score = DEFAULT_SCORE.toFloat()
|
||||
manga.status = DEFAULT_STATUS
|
||||
add(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 MangaSync.getAnilistStatus() = when (status) {
|
||||
READING -> "reading"
|
||||
COMPLETED -> "completed"
|
||||
ON_HOLD -> "on-hold"
|
||||
DROPPED -> "dropped"
|
||||
PLAN_TO_READ -> "plan to read"
|
||||
else -> throw NotImplementedError("Unknown status")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
package eu.kanade.tachiyomi.data.track
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
||||
|
||||
class TrackManager(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
const val MYANIMELIST = 1
|
||||
const val ANILIST = 2
|
||||
const val KITSU = 3
|
||||
}
|
||||
|
||||
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
||||
|
||||
val aniList = Anilist(context, ANILIST)
|
||||
|
||||
val kitsu = Kitsu(context, KITSU)
|
||||
|
||||
val services = listOf(myAnimeList, aniList, kitsu)
|
||||
|
||||
fun getService(id: Int) = services.find { it.id == id }
|
||||
|
||||
fun hasLoggedServices() = services.any { it.isLogged }
|
||||
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package eu.kanade.tachiyomi.data.track
|
||||
|
||||
import android.support.annotation.CallSuper
|
||||
import android.support.annotation.DrawableRes
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import okhttp3.OkHttpClient
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class TrackService(val id: Int) {
|
||||
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
val networkService: NetworkHelper by injectLazy()
|
||||
|
||||
open val client: OkHttpClient
|
||||
get() = networkService.client
|
||||
|
||||
// Name of the manga sync service to display
|
||||
abstract val name: String
|
||||
|
||||
abstract fun login(username: String, password: String): Completable
|
||||
|
||||
open val isLogged: Boolean
|
||||
get() = !getUsername().isEmpty() &&
|
||||
!getPassword().isEmpty()
|
||||
|
||||
abstract fun add(track: Track): Observable<Track>
|
||||
|
||||
abstract fun update(track: Track): Observable<Track>
|
||||
|
||||
abstract fun bind(track: Track): Observable<Track>
|
||||
|
||||
abstract fun search(query: String): Observable<List<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) {
|
||||
preferences.setTrackCredentials(this, username, password)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
open fun logout() {
|
||||
preferences.setTrackCredentials(this, "", "")
|
||||
}
|
||||
|
||||
fun getUsername() = preferences.trackUsername(this)
|
||||
|
||||
fun getPassword() = preferences.trackPassword(this)
|
||||
|
||||
}
|
@ -0,0 +1,191 @@
|
||||
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) {
|
||||
|
||||
companion object {
|
||||
const val READING = 1
|
||||
const val COMPLETED = 2
|
||||
const val ON_HOLD = 3
|
||||
const val DROPPED = 4
|
||||
const val PLAN_TO_READ = 5
|
||||
|
||||
const val DEFAULT_STATUS = READING
|
||||
const val DEFAULT_SCORE = 0
|
||||
}
|
||||
|
||||
override val name = "AniList"
|
||||
|
||||
private val interceptor by lazy { AnilistInterceptor(getPassword()) }
|
||||
|
||||
private val api by lazy {
|
||||
AnilistApi.createService(networkService.client.newBuilder()
|
||||
.addInterceptor(interceptor)
|
||||
.build())
|
||||
}
|
||||
|
||||
override fun getLogo() = R.drawable.al
|
||||
|
||||
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
||||
|
||||
override fun maxScore() = 100
|
||||
|
||||
override fun login(username: String, password: String) = login(password)
|
||||
|
||||
fun login(authCode: String): Completable {
|
||||
// Create a new api with the default client to avoid request interceptions.
|
||||
return AnilistApi.createService(client)
|
||||
// Request the access token from the API with the authorization code.
|
||||
.requestAccessToken(authCode)
|
||||
// Save the token in the interceptor.
|
||||
.doOnNext { interceptor.setAuth(it) }
|
||||
// Obtain the authenticated user from the API.
|
||||
.zipWith(api.getCurrentUser().map {
|
||||
preferences.anilistScoreType().set(it["score_type"].int)
|
||||
it["id"].string
|
||||
}, { oauth, user -> Pair(user, oauth.refresh_token!!) })
|
||||
// Save service credentials (username and refresh token).
|
||||
.doOnNext { saveCredentials(it.first, it.second) }
|
||||
// Logout on any error.
|
||||
.doOnError { logout() }
|
||||
.toCompletable()
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
super.logout()
|
||||
interceptor.setAuth(null)
|
||||
}
|
||||
|
||||
override fun search(query: String): Observable<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> {
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,89 +1,88 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.anilist
|
||||
|
||||
import android.net.Uri
|
||||
import com.google.gson.JsonObject
|
||||
import eu.kanade.tachiyomi.data.mangasync.anilist.model.ALManga
|
||||
import eu.kanade.tachiyomi.data.mangasync.anilist.model.ALUserLists
|
||||
import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.*
|
||||
import rx.Observable
|
||||
|
||||
interface AnilistApi {
|
||||
|
||||
companion object {
|
||||
private const val clientId = "tachiyomi-hrtje"
|
||||
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
|
||||
private const val clientUrl = "tachiyomi://anilist-auth"
|
||||
private const val baseUrl = "https://anilist.co/api/"
|
||||
|
||||
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon()
|
||||
.appendQueryParameter("grant_type", "authorization_code")
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("redirect_uri", clientUrl)
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.build()
|
||||
|
||||
fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token",
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.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,
|
||||
@Field("score_raw") score_raw: Int)
|
||||
: 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_raw") score_raw: Int)
|
||||
: Observable<Response<ResponseBody>>
|
||||
|
||||
package eu.kanade.tachiyomi.data.track.anilist
|
||||
|
||||
import android.net.Uri
|
||||
import com.google.gson.JsonObject
|
||||
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
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.*
|
||||
import rx.Observable
|
||||
|
||||
interface AnilistApi {
|
||||
|
||||
companion object {
|
||||
private const val clientId = "tachiyomi-hrtje"
|
||||
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
|
||||
private const val clientUrl = "tachiyomi://anilist-auth"
|
||||
private const val baseUrl = "https://anilist.co/api/"
|
||||
|
||||
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon()
|
||||
.appendQueryParameter("grant_type", "authorization_code")
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("redirect_uri", clientUrl)
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.build()
|
||||
|
||||
fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token",
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.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,61 +1,61 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.anilist
|
||||
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
||||
|
||||
/**
|
||||
* OAuth object used for authenticated requests.
|
||||
*
|
||||
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
|
||||
* before its original expiration date.
|
||||
*/
|
||||
private var oauth: OAuth? = null
|
||||
set(value) {
|
||||
field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
if (refreshToken.isNullOrEmpty()) {
|
||||
throw Exception("Not authenticated with Anilist")
|
||||
}
|
||||
|
||||
// Refresh access token if null or expired.
|
||||
if (oauth == null || oauth!!.isExpired()) {
|
||||
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
|
||||
oauth = if (response.isSuccessful) {
|
||||
Gson().fromJson(response.body().string(), OAuth::class.java)
|
||||
} else {
|
||||
response.close()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Throw on null auth.
|
||||
if (oauth == null) {
|
||||
throw Exception("Access token wasn't refreshed")
|
||||
}
|
||||
|
||||
// Add the authorization header to the original request.
|
||||
val authRequest = originalRequest.newBuilder()
|
||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||
.build()
|
||||
|
||||
return chain.proceed(authRequest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user authenticates with Anilist for the first time. Sets the refresh token
|
||||
* and the oauth object.
|
||||
*/
|
||||
fun setAuth(oauth: OAuth?) {
|
||||
refreshToken = oauth?.refresh_token
|
||||
this.oauth = oauth
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.data.track.anilist
|
||||
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.data.track.anilist.model.OAuth
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
||||
|
||||
/**
|
||||
* OAuth object used for authenticated requests.
|
||||
*
|
||||
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
|
||||
* before its original expiration date.
|
||||
*/
|
||||
private var oauth: OAuth? = null
|
||||
set(value) {
|
||||
field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
if (refreshToken.isNullOrEmpty()) {
|
||||
throw Exception("Not authenticated with Anilist")
|
||||
}
|
||||
|
||||
// Refresh access token if null or expired.
|
||||
if (oauth == null || oauth!!.isExpired()) {
|
||||
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
|
||||
oauth = if (response.isSuccessful) {
|
||||
Gson().fromJson(response.body().string(), OAuth::class.java)
|
||||
} else {
|
||||
response.close()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Throw on null auth.
|
||||
if (oauth == null) {
|
||||
throw Exception("Access token wasn't refreshed")
|
||||
}
|
||||
|
||||
// Add the authorization header to the original request.
|
||||
val authRequest = originalRequest.newBuilder()
|
||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||
.build()
|
||||
|
||||
return chain.proceed(authRequest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user authenticates with Anilist for the first time. Sets the refresh token
|
||||
* and the oauth object.
|
||||
*/
|
||||
fun setAuth(oauth: OAuth?) {
|
||||
refreshToken = oauth?.refresh_token
|
||||
this.oauth = oauth
|
||||
}
|
||||
|
||||
}
|
@ -1,17 +1,17 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||
|
||||
data class ALManga(
|
||||
val id: Int,
|
||||
val title_romaji: String,
|
||||
val type: String,
|
||||
val total_chapters: Int) {
|
||||
|
||||
fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply {
|
||||
remote_id = this@ALManga.id
|
||||
title = title_romaji
|
||||
total_chapters = this@ALManga.total_chapters
|
||||
}
|
||||
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 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
||||
|
||||
data class ALUserLists(val lists: Map<String, List<ALUserManga>>) {
|
||||
|
||||
fun flatten() = lists.values.flatten()
|
||||
package eu.kanade.tachiyomi.data.track.anilist.model
|
||||
|
||||
data class ALUserLists(val lists: Map<String, List<ALUserManga>>) {
|
||||
|
||||
fun flatten() = lists.values.flatten()
|
||||
}
|
@ -1,29 +1,29 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||
import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist
|
||||
|
||||
data class ALUserManga(
|
||||
val id: Int,
|
||||
val list_status: String,
|
||||
val score_raw: Int,
|
||||
val chapters_read: Int,
|
||||
val manga: ALManga) {
|
||||
|
||||
fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply {
|
||||
remote_id = manga.id
|
||||
status = getMangaSyncStatus()
|
||||
score = score_raw.toFloat()
|
||||
last_chapter_read = chapters_read
|
||||
}
|
||||
|
||||
fun getMangaSyncStatus() = 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")
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
||||
|
||||
data class OAuth(
|
||||
val access_token: String,
|
||||
val token_type: String,
|
||||
val expires: Long,
|
||||
val expires_in: Long,
|
||||
val refresh_token: String?) {
|
||||
|
||||
fun isExpired() = System.currentTimeMillis() > expires
|
||||
package eu.kanade.tachiyomi.data.track.anilist.model
|
||||
|
||||
data class OAuth(
|
||||
val access_token: String,
|
||||
val token_type: String,
|
||||
val expires: Long,
|
||||
val expires_in: Long,
|
||||
val refresh_token: String?) {
|
||||
|
||||
fun isExpired() = System.currentTimeMillis() > expires
|
||||
}
|
@ -0,0 +1,219 @@
|
||||
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) {
|
||||
|
||||
companion object {
|
||||
const val READING = 1
|
||||
const val COMPLETED = 2
|
||||
const val ON_HOLD = 3
|
||||
const val DROPPED = 4
|
||||
const val PLAN_TO_READ = 5
|
||||
|
||||
const val DEFAULT_STATUS = READING
|
||||
const val DEFAULT_SCORE = 0f
|
||||
}
|
||||
|
||||
override val name = "Kitsu"
|
||||
|
||||
private val gson: Gson by injectLazy()
|
||||
|
||||
private val interceptor by lazy { KitsuInterceptor(this, gson) }
|
||||
|
||||
private val api by lazy {
|
||||
KitsuApi.createService(client.newBuilder()
|
||||
.addInterceptor(interceptor)
|
||||
.build())
|
||||
}
|
||||
|
||||
private fun getUserId(): String {
|
||||
return getPassword()
|
||||
}
|
||||
|
||||
fun saveToken(oauth: OAuth?) {
|
||||
val json = gson.toJson(oauth)
|
||||
preferences.trackToken(this).set(json)
|
||||
}
|
||||
|
||||
fun restoreToken(): OAuth? {
|
||||
return try {
|
||||
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun login(username: String, password: String): Completable {
|
||||
return KitsuApi.createLoginService(client)
|
||||
.requestAccessToken(username, password)
|
||||
.doOnNext { interceptor.newAuth(it) }
|
||||
.flatMap { api.getCurrentUser().map { it["data"].array[0]["id"].string } }
|
||||
.doOnNext { userId -> saveCredentials(username, userId) }
|
||||
.doOnError { logout() }
|
||||
.toCompletable()
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
super.logout()
|
||||
interceptor.newAuth(null)
|
||||
}
|
||||
|
||||
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> {
|
||||
return find(track)
|
||||
.flatMap { remoteTrack ->
|
||||
if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.remote_id = remoteTrack.remote_id
|
||||
update(track)
|
||||
} else {
|
||||
track.score = DEFAULT_SCORE
|
||||
track.status = DEFAULT_STATUS
|
||||
add(track)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
package eu.kanade.tachiyomi.data.track.kitsu
|
||||
|
||||
import com.google.gson.JsonObject
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.*
|
||||
import rx.Observable
|
||||
|
||||
interface KitsuApi {
|
||||
|
||||
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)
|
||||
|
||||
fun createLoginService(client: OkHttpClient) = Retrofit.Builder()
|
||||
.baseUrl(loginUrl)
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||
.build()
|
||||
.create(KitsuApi::class.java)
|
||||
|
||||
fun refreshTokenRequest(token: String) = POST("${loginUrl}oauth/token",
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("client_id", clientId)
|
||||
.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>
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package eu.kanade.tachiyomi.data.track.kitsu
|
||||
|
||||
import com.google.gson.Gson
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor {
|
||||
|
||||
/**
|
||||
* OAuth object used for authenticated requests.
|
||||
*/
|
||||
private var oauth: OAuth? = kitsu.restoreToken()
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
val currAuth = oauth ?: throw Exception("Not authenticated with Kitsu")
|
||||
|
||||
val refreshToken = currAuth.refresh_token!!
|
||||
|
||||
// Refresh access token if expired.
|
||||
if (currAuth.isExpired()) {
|
||||
val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken))
|
||||
if (response.isSuccessful) {
|
||||
newAuth(gson.fromJson(response.body().string(), OAuth::class.java))
|
||||
} else {
|
||||
response.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Add the authorization header to the original request.
|
||||
val authRequest = originalRequest.newBuilder()
|
||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||
.header("Accept", "application/vnd.api+json")
|
||||
.header("Content-Type", "application/vnd.api+json")
|
||||
.build()
|
||||
|
||||
return chain.proceed(authRequest)
|
||||
}
|
||||
|
||||
fun newAuth(oauth: OAuth?) {
|
||||
this.oauth = oauth
|
||||
kitsu.saveToken(oauth)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package eu.kanade.tachiyomi.data.track.kitsu
|
||||
|
||||
import android.support.annotation.CallSuper
|
||||
import com.github.salomonbrys.kotson.*
|
||||
import com.google.gson.JsonObject
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
|
||||
open class KitsuManga(obj: JsonObject) {
|
||||
val id by obj.byInt
|
||||
val canonicalTitle by obj["attributes"].byString
|
||||
val chapterCount = obj["attributes"]["chapterCount"].nullInt
|
||||
|
||||
@CallSuper
|
||||
open fun toTrack() = Track.create(TrackManager.KITSU).apply {
|
||||
remote_id = this@KitsuManga.id
|
||||
title = canonicalTitle
|
||||
total_chapters = chapterCount ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
class KitsuLibManga(obj: JsonObject, manga: JsonObject) : KitsuManga(manga) {
|
||||
val remoteId by obj.byInt("id")
|
||||
val status by obj["attributes"].byString
|
||||
val rating = obj["attributes"]["rating"].nullString
|
||||
val progress by obj["attributes"].byInt
|
||||
|
||||
override fun toTrack() = super.toTrack().apply {
|
||||
remote_id = remoteId
|
||||
status = toTrackStatus()
|
||||
score = rating?.let { it.toFloat() * 2 } ?: 0f
|
||||
last_chapter_read = progress
|
||||
}
|
||||
|
||||
private fun toTrackStatus() = when (status) {
|
||||
"current" -> Kitsu.READING
|
||||
"completed" -> Kitsu.COMPLETED
|
||||
"on_hold" -> Kitsu.ON_HOLD
|
||||
"dropped" -> Kitsu.DROPPED
|
||||
"planned" -> Kitsu.PLAN_TO_READ
|
||||
else -> throw Exception("Unknown status")
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package eu.kanade.tachiyomi.data.track.kitsu
|
||||
|
||||
data class OAuth(
|
||||
val access_token: String,
|
||||
val token_type: String,
|
||||
val created_at: Long,
|
||||
val expires_in: Long,
|
||||
val refresh_token: String?) {
|
||||
|
||||
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
|
||||
}
|
@ -1,222 +1,263 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.myanimelist
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Xml
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
||||
import eu.kanade.tachiyomi.data.network.GET
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import eu.kanade.tachiyomi.data.network.asObservable
|
||||
import eu.kanade.tachiyomi.util.selectInt
|
||||
import eu.kanade.tachiyomi.util.selectText
|
||||
import okhttp3.Credentials
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.RequestBody
|
||||
import org.jsoup.Jsoup
|
||||
import org.xmlpull.v1.XmlSerializer
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import java.io.StringWriter
|
||||
|
||||
class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) {
|
||||
|
||||
private lateinit var headers: Headers
|
||||
|
||||
companion object {
|
||||
val BASE_URL = "https://myanimelist.net"
|
||||
|
||||
private val ENTRY_TAG = "entry"
|
||||
private val CHAPTER_TAG = "chapter"
|
||||
private val SCORE_TAG = "score"
|
||||
private val STATUS_TAG = "status"
|
||||
|
||||
val READING = 1
|
||||
val COMPLETED = 2
|
||||
val ON_HOLD = 3
|
||||
val DROPPED = 4
|
||||
val PLAN_TO_READ = 6
|
||||
|
||||
val DEFAULT_STATUS = READING
|
||||
val DEFAULT_SCORE = 0
|
||||
}
|
||||
|
||||
init {
|
||||
val username = getUsername()
|
||||
val password = getPassword()
|
||||
|
||||
if (!username.isEmpty() && !password.isEmpty()) {
|
||||
createHeaders(username, password)
|
||||
}
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "MyAnimeList"
|
||||
|
||||
fun getLoginUrl() = Uri.parse(BASE_URL).buildUpon()
|
||||
.appendEncodedPath("api/account/verify_credentials.xml")
|
||||
.toString()
|
||||
|
||||
fun getSearchUrl(query: String) = Uri.parse(BASE_URL).buildUpon()
|
||||
.appendEncodedPath("api/manga/search.xml")
|
||||
.appendQueryParameter("q", query)
|
||||
.toString()
|
||||
|
||||
fun getListUrl(username: String) = Uri.parse(BASE_URL).buildUpon()
|
||||
.appendPath("malappinfo.php")
|
||||
.appendQueryParameter("u", username)
|
||||
.appendQueryParameter("status", "all")
|
||||
.appendQueryParameter("type", "manga")
|
||||
.toString()
|
||||
|
||||
fun getUpdateUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
|
||||
.appendEncodedPath("api/mangalist/update")
|
||||
.appendPath("${manga.remote_id}.xml")
|
||||
.toString()
|
||||
|
||||
fun getAddUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
|
||||
.appendEncodedPath("api/mangalist/add")
|
||||
.appendPath("${manga.remote_id}.xml")
|
||||
.toString()
|
||||
|
||||
override fun login(username: String, password: String): Completable {
|
||||
createHeaders(username, password)
|
||||
return client.newCall(GET(getLoginUrl(), headers))
|
||||
.asObservable()
|
||||
.doOnNext { it.close() }
|
||||
.doOnNext { if (it.code() != 200) throw Exception("Login error") }
|
||||
.toCompletable()
|
||||
}
|
||||
|
||||
fun search(query: String): Observable<List<MangaSync>> {
|
||||
return client.newCall(GET(getSearchUrl(query), headers))
|
||||
.asObservable()
|
||||
.map { Jsoup.parse(it.body().string()) }
|
||||
.flatMap { Observable.from(it.select("entry")) }
|
||||
.filter { it.select("type").text() != "Novel" }
|
||||
.map {
|
||||
MangaSync.create(id).apply {
|
||||
title = it.selectText("title")!!
|
||||
remote_id = it.selectInt("id")
|
||||
total_chapters = it.selectInt("chapters")
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
// MAL doesn't support score with decimals
|
||||
fun getList(): Observable<List<MangaSync>> {
|
||||
return networkService.forceCacheClient
|
||||
.newCall(GET(getListUrl(getUsername()), headers))
|
||||
.asObservable()
|
||||
.map { Jsoup.parse(it.body().string()) }
|
||||
.flatMap { Observable.from(it.select("manga")) }
|
||||
.map {
|
||||
MangaSync.create(id).apply {
|
||||
title = it.selectText("series_title")!!
|
||||
remote_id = it.selectInt("series_mangadb_id")
|
||||
last_chapter_read = it.selectInt("my_read_chapters")
|
||||
status = it.selectInt("my_status")
|
||||
score = it.selectInt("my_score").toFloat()
|
||||
total_chapters = it.selectInt("series_chapters")
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
override fun update(manga: MangaSync): Observable<MangaSync> {
|
||||
return Observable.defer {
|
||||
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
|
||||
manga.status = COMPLETED
|
||||
}
|
||||
client.newCall(POST(getUpdateUrl(manga), headers, getMangaPostPayload(manga)))
|
||||
.asObservable()
|
||||
.doOnNext { it.close() }
|
||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
|
||||
.map { manga }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun add(manga: MangaSync): Observable<MangaSync> {
|
||||
return Observable.defer {
|
||||
client.newCall(POST(getAddUrl(manga), headers, getMangaPostPayload(manga)))
|
||||
.asObservable()
|
||||
.doOnNext { it.close() }
|
||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
|
||||
.map { manga }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMangaPostPayload(manga: MangaSync): RequestBody {
|
||||
val xml = Xml.newSerializer()
|
||||
val writer = StringWriter()
|
||||
|
||||
with(xml) {
|
||||
setOutput(writer)
|
||||
startDocument("UTF-8", false)
|
||||
startTag("", ENTRY_TAG)
|
||||
|
||||
// Last chapter read
|
||||
if (manga.last_chapter_read != 0) {
|
||||
inTag(CHAPTER_TAG, manga.last_chapter_read.toString())
|
||||
}
|
||||
// Manga status in the list
|
||||
inTag(STATUS_TAG, manga.status.toString())
|
||||
|
||||
// Manga score
|
||||
inTag(SCORE_TAG, manga.score.toString())
|
||||
|
||||
endTag("", ENTRY_TAG)
|
||||
endDocument()
|
||||
}
|
||||
|
||||
val form = FormBody.Builder()
|
||||
form.add("data", writer.toString())
|
||||
return form.build()
|
||||
}
|
||||
|
||||
fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
|
||||
startTag(namespace, tag)
|
||||
text(body)
|
||||
endTag(namespace, tag)
|
||||
}
|
||||
|
||||
override fun bind(manga: MangaSync): Observable<MangaSync> {
|
||||
return getList()
|
||||
.flatMap { userlist ->
|
||||
manga.sync_id = id
|
||||
val mangaFromList = userlist.find { it.remote_id == manga.remote_id }
|
||||
if (mangaFromList != null) {
|
||||
manga.copyPersonalFrom(mangaFromList)
|
||||
update(manga)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
manga.score = DEFAULT_SCORE.toFloat()
|
||||
manga.status = DEFAULT_STATUS
|
||||
add(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 -> ""
|
||||
}
|
||||
}
|
||||
|
||||
fun createHeaders(username: String, password: String) {
|
||||
val builder = Headers.Builder()
|
||||
builder.add("Authorization", Credentials.basic(username, password))
|
||||
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
|
||||
headers = builder.build()
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.util.Xml
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.network.GET
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import eu.kanade.tachiyomi.data.network.asObservable
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.util.selectInt
|
||||
import eu.kanade.tachiyomi.util.selectText
|
||||
import okhttp3.Credentials
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.RequestBody
|
||||
import org.jsoup.Jsoup
|
||||
import org.xmlpull.v1.XmlSerializer
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import java.io.StringWriter
|
||||
|
||||
class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
private lateinit var headers: Headers
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://myanimelist.net"
|
||||
|
||||
private val ENTRY_TAG = "entry"
|
||||
private val CHAPTER_TAG = "chapter"
|
||||
private val SCORE_TAG = "score"
|
||||
private val STATUS_TAG = "status"
|
||||
|
||||
const val READING = 1
|
||||
const val COMPLETED = 2
|
||||
const val ON_HOLD = 3
|
||||
const val DROPPED = 4
|
||||
const val PLAN_TO_READ = 6
|
||||
|
||||
const val DEFAULT_STATUS = READING
|
||||
const val DEFAULT_SCORE = 0
|
||||
|
||||
const val PREFIX_MY = "my:"
|
||||
}
|
||||
|
||||
init {
|
||||
val username = getUsername()
|
||||
val password = getPassword()
|
||||
|
||||
if (!username.isEmpty() && !password.isEmpty()) {
|
||||
createHeaders(username, password)
|
||||
}
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "MyAnimeList"
|
||||
|
||||
override fun getLogo() = R.drawable.mal
|
||||
|
||||
override fun getLogoColor() = Color.rgb(46, 81, 162)
|
||||
|
||||
override fun maxScore() = 10
|
||||
|
||||
override fun formatScore(track: Track): String {
|
||||
return track.score.toInt().toString()
|
||||
}
|
||||
|
||||
fun getLoginUrl() = Uri.parse(BASE_URL).buildUpon()
|
||||
.appendEncodedPath("api/account/verify_credentials.xml")
|
||||
.toString()
|
||||
|
||||
fun getSearchUrl(query: String) = Uri.parse(BASE_URL).buildUpon()
|
||||
.appendEncodedPath("api/manga/search.xml")
|
||||
.appendQueryParameter("q", query)
|
||||
.toString()
|
||||
|
||||
fun getListUrl(username: String) = Uri.parse(BASE_URL).buildUpon()
|
||||
.appendPath("malappinfo.php")
|
||||
.appendQueryParameter("u", username)
|
||||
.appendQueryParameter("status", "all")
|
||||
.appendQueryParameter("type", "manga")
|
||||
.toString()
|
||||
|
||||
fun getUpdateUrl(track: Track) = Uri.parse(BASE_URL).buildUpon()
|
||||
.appendEncodedPath("api/mangalist/update")
|
||||
.appendPath("${track.remote_id}.xml")
|
||||
.toString()
|
||||
|
||||
fun getAddUrl(track: Track) = Uri.parse(BASE_URL).buildUpon()
|
||||
.appendEncodedPath("api/mangalist/add")
|
||||
.appendPath("${track.remote_id}.xml")
|
||||
.toString()
|
||||
|
||||
override fun login(username: String, password: String): Completable {
|
||||
createHeaders(username, password)
|
||||
return client.newCall(GET(getLoginUrl(), headers))
|
||||
.asObservable()
|
||||
.doOnNext { it.close() }
|
||||
.doOnNext { if (it.code() != 200) throw Exception("Login error") }
|
||||
.doOnNext { saveCredentials(username, password) }
|
||||
.doOnError { logout() }
|
||||
.toCompletable()
|
||||
}
|
||||
|
||||
override fun search(query: String): Observable<List<Track>> {
|
||||
return if (query.startsWith(PREFIX_MY)) {
|
||||
val realQuery = query.substring(PREFIX_MY.length).toLowerCase().trim()
|
||||
getList()
|
||||
.flatMap { Observable.from(it) }
|
||||
.filter { realQuery in it.title.toLowerCase() }
|
||||
.toList()
|
||||
} else {
|
||||
client.newCall(GET(getSearchUrl(query), headers))
|
||||
.asObservable()
|
||||
.map { Jsoup.parse(it.body().string()) }
|
||||
.flatMap { Observable.from(it.select("entry")) }
|
||||
.filter { it.select("type").text() != "Novel" }
|
||||
.map {
|
||||
Track.create(id).apply {
|
||||
title = it.selectText("title")!!
|
||||
remote_id = it.selectInt("id")
|
||||
total_chapters = it.selectInt("chapters")
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MAL doesn't support score with decimals
|
||||
fun getList(): Observable<List<Track>> {
|
||||
return networkService.forceCacheClient
|
||||
.newCall(GET(getListUrl(getUsername()), headers))
|
||||
.asObservable()
|
||||
.map { Jsoup.parse(it.body().string()) }
|
||||
.flatMap { Observable.from(it.select("manga")) }
|
||||
.map {
|
||||
Track.create(id).apply {
|
||||
title = it.selectText("series_title")!!
|
||||
remote_id = it.selectInt("series_mangadb_id")
|
||||
last_chapter_read = it.selectInt("my_read_chapters")
|
||||
status = it.selectInt("my_status")
|
||||
score = it.selectInt("my_score").toFloat()
|
||||
total_chapters = it.selectInt("series_chapters")
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
override fun update(track: Track): Observable<Track> {
|
||||
return Observable.defer {
|
||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||
track.status = COMPLETED
|
||||
}
|
||||
client.newCall(POST(getUpdateUrl(track), headers, getMangaPostPayload(track)))
|
||||
.asObservable()
|
||||
.doOnNext { it.close() }
|
||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
|
||||
.map { track }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun add(track: Track): Observable<Track> {
|
||||
return Observable.defer {
|
||||
client.newCall(POST(getAddUrl(track), headers, getMangaPostPayload(track)))
|
||||
.asObservable()
|
||||
.doOnNext { it.close() }
|
||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
|
||||
.map { track }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMangaPostPayload(track: Track): RequestBody {
|
||||
val xml = Xml.newSerializer()
|
||||
val writer = StringWriter()
|
||||
|
||||
with(xml) {
|
||||
setOutput(writer)
|
||||
startDocument("UTF-8", false)
|
||||
startTag("", ENTRY_TAG)
|
||||
|
||||
// Last chapter read
|
||||
if (track.last_chapter_read != 0) {
|
||||
inTag(CHAPTER_TAG, track.last_chapter_read.toString())
|
||||
}
|
||||
// Manga status in the list
|
||||
inTag(STATUS_TAG, track.status.toString())
|
||||
|
||||
// Manga score
|
||||
inTag(SCORE_TAG, track.score.toString())
|
||||
|
||||
endTag("", ENTRY_TAG)
|
||||
endDocument()
|
||||
}
|
||||
|
||||
val form = FormBody.Builder()
|
||||
form.add("data", writer.toString())
|
||||
return form.build()
|
||||
}
|
||||
|
||||
fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
|
||||
startTag(namespace, tag)
|
||||
text(body)
|
||||
endTag(namespace, tag)
|
||||
}
|
||||
|
||||
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 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))
|
||||
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
|
||||
headers = builder.build()
|
||||
}
|
||||
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.myanimelist
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.view.View
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
|
||||
import kotlinx.android.synthetic.main.dialog_myanimelist_search.view.*
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.subjects.PublishSubject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MyAnimeListDialogFragment : DialogFragment() {
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(): MyAnimeListDialogFragment {
|
||||
return MyAnimeListDialogFragment()
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var v: View
|
||||
|
||||
lateinit var adapter: MyAnimeListSearchAdapter
|
||||
private set
|
||||
|
||||
lateinit var querySubject: PublishSubject<String>
|
||||
private set
|
||||
|
||||
private var selectedItem: MangaSync? = null
|
||||
|
||||
private var searchSubscription: Subscription? = null
|
||||
|
||||
override fun onCreateDialog(savedState: Bundle?): Dialog {
|
||||
val dialog = MaterialDialog.Builder(activity)
|
||||
.customView(R.layout.dialog_myanimelist_search, false)
|
||||
.positiveText(android.R.string.ok)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.onPositive { dialog1, which -> onPositiveButtonClick() }
|
||||
.build()
|
||||
|
||||
onViewCreated(dialog.view, savedState)
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||
v = view
|
||||
|
||||
// Create adapter
|
||||
adapter = MyAnimeListSearchAdapter(activity)
|
||||
view.myanimelist_search_results.adapter = adapter
|
||||
|
||||
// Set listeners
|
||||
view.myanimelist_search_results.setOnItemClickListener { parent, viewList, position, id ->
|
||||
selectedItem = adapter.getItem(position)
|
||||
}
|
||||
|
||||
// Do an initial search based on the manga's title
|
||||
if (savedState == null) {
|
||||
val title = presenter.manga.title
|
||||
view.myanimelist_search_field.append(title)
|
||||
search(title)
|
||||
}
|
||||
|
||||
querySubject = PublishSubject.create<String>()
|
||||
|
||||
view.myanimelist_search_field.addTextChangedListener(object : SimpleTextWatcher() {
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
querySubject.onNext(s.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Listen to text changes
|
||||
searchSubscription = querySubject.debounce(1, TimeUnit.SECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { search(it) }
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
searchSubscription?.unsubscribe()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun onPositiveButtonClick() {
|
||||
presenter.registerManga(selectedItem)
|
||||
}
|
||||
|
||||
private fun search(query: String) {
|
||||
if (!query.isNullOrEmpty()) {
|
||||
v.myanimelist_search_results.visibility = View.GONE
|
||||
v.progress.visibility = View.VISIBLE
|
||||
presenter.searchManga(query)
|
||||
}
|
||||
}
|
||||
|
||||
fun onSearchResults(results: List<MangaSync>) {
|
||||
selectedItem = null
|
||||
v.progress.visibility = View.GONE
|
||||
v.myanimelist_search_results.visibility = View.VISIBLE
|
||||
adapter.setItems(results)
|
||||
}
|
||||
|
||||
fun onSearchResultsError() {
|
||||
v.progress.visibility = View.GONE
|
||||
v.myanimelist_search_results.visibility = View.VISIBLE
|
||||
adapter.clear()
|
||||
}
|
||||
|
||||
val malFragment: MyAnimeListFragment
|
||||
get() = parentFragment as MyAnimeListFragment
|
||||
|
||||
val presenter: MyAnimeListPresenter
|
||||
get() = malFragment.presenter
|
||||
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.myanimelist
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.NumberPicker
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import kotlinx.android.synthetic.main.card_myanimelist_personal.*
|
||||
import kotlinx.android.synthetic.main.fragment_myanimelist.*
|
||||
import nucleus.factory.RequiresPresenter
|
||||
import java.text.DecimalFormat
|
||||
|
||||
@RequiresPresenter(MyAnimeListPresenter::class)
|
||||
class MyAnimeListFragment : BaseRxFragment<MyAnimeListPresenter>() {
|
||||
|
||||
companion object {
|
||||
fun newInstance(): MyAnimeListFragment {
|
||||
return MyAnimeListFragment()
|
||||
}
|
||||
}
|
||||
|
||||
private var dialog: MyAnimeListDialogFragment? = null
|
||||
|
||||
private val decimalFormat = DecimalFormat("#.##")
|
||||
|
||||
private val SEARCH_FRAGMENT_TAG = "mal_search"
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_myanimelist, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||
swipe_refresh.isEnabled = false
|
||||
swipe_refresh.setOnRefreshListener { presenter.refresh() }
|
||||
myanimelist_title_layout.setOnClickListener { onTitleClick() }
|
||||
myanimelist_status_layout.setOnClickListener { onStatusClick() }
|
||||
myanimelist_chapters_layout.setOnClickListener { onChaptersClick() }
|
||||
myanimelist_score_layout.setOnClickListener { onScoreClick() }
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun setMangaSync(mangaSync: MangaSync?) {
|
||||
swipe_refresh.isEnabled = mangaSync != null
|
||||
mangaSync?.let {
|
||||
myanimelist_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary)
|
||||
myanimelist_title.setAllCaps(false)
|
||||
myanimelist_title.text = it.title
|
||||
myanimelist_chapters.text = if (it.total_chapters > 0)
|
||||
"${it.last_chapter_read}/${it.total_chapters}" else "${it.last_chapter_read}/-"
|
||||
myanimelist_score.text = if (it.score == 0f) "-" else decimalFormat.format(it.score)
|
||||
myanimelist_status.text = presenter.myAnimeList.getStatus(it.status)
|
||||
} ?: run {
|
||||
myanimelist_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button)
|
||||
myanimelist_title.setText(R.string.action_edit)
|
||||
myanimelist_chapters.text = ""
|
||||
myanimelist_score.text = ""
|
||||
myanimelist_status.text = ""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun onRefreshDone() {
|
||||
swipe_refresh.isRefreshing = false
|
||||
}
|
||||
|
||||
fun onRefreshError(error: Throwable) {
|
||||
swipe_refresh.isRefreshing = false
|
||||
context.toast(error.message)
|
||||
}
|
||||
|
||||
fun setSearchResults(results: List<MangaSync>) {
|
||||
findSearchFragmentIfNeeded()
|
||||
|
||||
dialog?.onSearchResults(results)
|
||||
}
|
||||
|
||||
fun setSearchResultsError(error: Throwable) {
|
||||
findSearchFragmentIfNeeded()
|
||||
context.toast(error.message)
|
||||
|
||||
dialog?.onSearchResultsError()
|
||||
}
|
||||
|
||||
private fun findSearchFragmentIfNeeded() {
|
||||
if (dialog == null) {
|
||||
dialog = childFragmentManager.findFragmentByTag(SEARCH_FRAGMENT_TAG) as MyAnimeListDialogFragment
|
||||
}
|
||||
}
|
||||
|
||||
fun onTitleClick() {
|
||||
if (dialog == null) {
|
||||
dialog = MyAnimeListDialogFragment.newInstance()
|
||||
}
|
||||
|
||||
presenter.restartSearch()
|
||||
dialog?.show(childFragmentManager, SEARCH_FRAGMENT_TAG)
|
||||
}
|
||||
|
||||
fun onStatusClick() {
|
||||
if (presenter.mangaSync == null)
|
||||
return
|
||||
|
||||
MaterialDialog.Builder(activity)
|
||||
.title(R.string.status)
|
||||
.items(presenter.getAllStatus())
|
||||
.itemsCallbackSingleChoice(presenter.getIndexFromStatus(), { dialog, view, i, charSequence ->
|
||||
presenter.setStatus(i)
|
||||
myanimelist_status.text = "..."
|
||||
true
|
||||
})
|
||||
.show()
|
||||
}
|
||||
|
||||
fun onChaptersClick() {
|
||||
if (presenter.mangaSync == null)
|
||||
return
|
||||
|
||||
val dialog = MaterialDialog.Builder(activity)
|
||||
.title(R.string.chapters)
|
||||
.customView(R.layout.dialog_myanimelist_chapters, false)
|
||||
.positiveText(android.R.string.ok)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.onPositive { d, action ->
|
||||
val view = d.customView
|
||||
if (view != null) {
|
||||
val np = view.findViewById(R.id.chapters_picker) as NumberPicker
|
||||
np.clearFocus()
|
||||
presenter.setLastChapterRead(np.value)
|
||||
myanimelist_chapters.text = "..."
|
||||
}
|
||||
}
|
||||
.show()
|
||||
|
||||
val view = dialog.customView
|
||||
if (view != null) {
|
||||
val np = view.findViewById(R.id.chapters_picker) as NumberPicker
|
||||
// Set initial value
|
||||
np.value = presenter.mangaSync!!.last_chapter_read
|
||||
// Don't allow to go from 0 to 9999
|
||||
np.wrapSelectorWheel = false
|
||||
}
|
||||
}
|
||||
|
||||
fun onScoreClick() {
|
||||
if (presenter.mangaSync == null)
|
||||
return
|
||||
|
||||
val dialog = MaterialDialog.Builder(activity)
|
||||
.title(R.string.score)
|
||||
.customView(R.layout.dialog_myanimelist_score, false)
|
||||
.positiveText(android.R.string.ok)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.onPositive { d, action ->
|
||||
val view = d.customView
|
||||
if (view != null) {
|
||||
val np = view.findViewById(R.id.score_picker) as NumberPicker
|
||||
np.clearFocus()
|
||||
presenter.setScore(np.value)
|
||||
myanimelist_score.text = "..."
|
||||
}
|
||||
}
|
||||
.show()
|
||||
|
||||
val view = dialog.customView
|
||||
if (view != null) {
|
||||
val np = view.findViewById(R.id.score_picker) as NumberPicker
|
||||
// Set initial value
|
||||
np.value = presenter.mangaSync!!.score.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.myanimelist
|
||||
|
||||
import android.os.Bundle
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
||||
import eu.kanade.tachiyomi.util.SharedData
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MyAnimeListPresenter : BasePresenter<MyAnimeListFragment>() {
|
||||
|
||||
val db: DatabaseHelper by injectLazy()
|
||||
val syncManager: MangaSyncManager by injectLazy()
|
||||
|
||||
val myAnimeList by lazy { syncManager.myAnimeList }
|
||||
|
||||
lateinit var manga: Manga
|
||||
private set
|
||||
|
||||
var mangaSync: MangaSync? = null
|
||||
private set
|
||||
|
||||
private var query: String? = null
|
||||
|
||||
private val GET_MANGA_SYNC = 1
|
||||
private val GET_SEARCH_RESULTS = 2
|
||||
private val REFRESH = 3
|
||||
|
||||
private val PREFIX_MY = "my:"
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
startableLatestCache(GET_MANGA_SYNC,
|
||||
{ db.getMangaSync(manga, myAnimeList).asRxObservable()
|
||||
.doOnNext { mangaSync = it }
|
||||
.observeOn(AndroidSchedulers.mainThread()) },
|
||||
{ view, mangaSync -> view.setMangaSync(mangaSync) })
|
||||
|
||||
startableLatestCache(GET_SEARCH_RESULTS,
|
||||
{ getSearchResultsObservable() },
|
||||
{ view, results -> view.setSearchResults(results) },
|
||||
{ view, error -> view.setSearchResultsError(error) })
|
||||
|
||||
startableFirst(REFRESH,
|
||||
{ getRefreshObservable() },
|
||||
{ view, result -> view.onRefreshDone() },
|
||||
{ view, error -> view.onRefreshError(error) })
|
||||
|
||||
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
|
||||
start(GET_MANGA_SYNC)
|
||||
}
|
||||
|
||||
fun getSearchResultsObservable(): Observable<List<MangaSync>> {
|
||||
return query?.let { query ->
|
||||
val observable: Observable<List<MangaSync>>
|
||||
if (query.startsWith(PREFIX_MY)) {
|
||||
val realQuery = query.substring(PREFIX_MY.length).toLowerCase().trim()
|
||||
observable = myAnimeList.getList()
|
||||
.flatMap { Observable.from(it) }
|
||||
.filter { it.title.toLowerCase().contains(realQuery) }
|
||||
.toList()
|
||||
} else {
|
||||
observable = myAnimeList.search(query)
|
||||
}
|
||||
observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
|
||||
} ?: Observable.error(Exception("Null query"))
|
||||
|
||||
}
|
||||
|
||||
fun getRefreshObservable(): Observable<PutResult> {
|
||||
return mangaSync?.let { mangaSync ->
|
||||
myAnimeList.getList()
|
||||
.map { myList ->
|
||||
myList.find { it.remote_id == mangaSync.remote_id }?.let {
|
||||
mangaSync.copyPersonalFrom(it)
|
||||
mangaSync.total_chapters = it.total_chapters
|
||||
mangaSync
|
||||
} ?: throw Exception("Could not find manga")
|
||||
}
|
||||
.flatMap { db.insertMangaSync(it).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
} ?: Observable.error(Exception("Not found"))
|
||||
}
|
||||
|
||||
private fun updateRemote() {
|
||||
mangaSync?.let { mangaSync ->
|
||||
add(myAnimeList.update(mangaSync)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap { db.insertMangaSync(mangaSync).asRxObservable() }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ next -> },
|
||||
{ error ->
|
||||
Timber.e(error)
|
||||
// Restart on error to set old values
|
||||
start(GET_MANGA_SYNC)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fun searchManga(query: String) {
|
||||
if (query.isNullOrEmpty() || query == this.query)
|
||||
return
|
||||
|
||||
this.query = query
|
||||
start(GET_SEARCH_RESULTS)
|
||||
}
|
||||
|
||||
fun restartSearch() {
|
||||
query = null
|
||||
stop(GET_SEARCH_RESULTS)
|
||||
}
|
||||
|
||||
fun registerManga(sync: MangaSync?) {
|
||||
if (sync != null) {
|
||||
sync.manga_id = manga.id!!
|
||||
add(myAnimeList.bind(sync)
|
||||
.flatMap { db.insertMangaSync(sync).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ },
|
||||
{ error -> context.toast(error.message) }))
|
||||
} else {
|
||||
db.deleteMangaSyncForManga(manga).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllStatus(): List<String> {
|
||||
return listOf(context.getString(R.string.reading),
|
||||
context.getString(R.string.completed),
|
||||
context.getString(R.string.on_hold),
|
||||
context.getString(R.string.dropped),
|
||||
context.getString(R.string.plan_to_read))
|
||||
}
|
||||
|
||||
fun getIndexFromStatus(): Int {
|
||||
return mangaSync?.let { mangaSync ->
|
||||
if (mangaSync.status == 6) 4 else mangaSync.status - 1
|
||||
} ?: 0
|
||||
}
|
||||
|
||||
fun setStatus(index: Int) {
|
||||
mangaSync?.status = if (index == 4) 6 else index + 1
|
||||
updateRemote()
|
||||
}
|
||||
|
||||
fun setScore(score: Int) {
|
||||
mangaSync?.score = score.toFloat()
|
||||
updateRemote()
|
||||
}
|
||||
|
||||
fun setLastChapterRead(chapterNumber: Int) {
|
||||
mangaSync?.last_chapter_read = chapterNumber
|
||||
updateRemote()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
if (mangaSync != null) {
|
||||
start(REFRESH)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.myanimelist
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import kotlinx.android.synthetic.main.dialog_myanimelist_search_item.view.*
|
||||
import java.util.*
|
||||
|
||||
class MyAnimeListSearchAdapter(context: Context) :
|
||||
ArrayAdapter<MangaSync>(context, R.layout.dialog_myanimelist_search_item, ArrayList<MangaSync>()) {
|
||||
|
||||
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
|
||||
var v = view
|
||||
// Get the data item for this position
|
||||
val sync = getItem(position)
|
||||
// Check if an existing view is being reused, otherwise inflate the view
|
||||
val holder: SearchViewHolder // view lookup cache stored in tag
|
||||
if (v == null) {
|
||||
v = parent.inflate(R.layout.dialog_myanimelist_search_item)
|
||||
holder = SearchViewHolder(v)
|
||||
v.tag = holder
|
||||
} else {
|
||||
holder = v.tag as SearchViewHolder
|
||||
}
|
||||
holder.onSetValues(sync)
|
||||
return v
|
||||
}
|
||||
|
||||
fun setItems(syncs: List<MangaSync>) {
|
||||
setNotifyOnChange(false)
|
||||
clear()
|
||||
addAll(syncs)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
class SearchViewHolder(private val view: View) {
|
||||
|
||||
fun onSetValues(sync: MangaSync) {
|
||||
view.myanimelist_result_title.text = sync.title
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.ViewGroup
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
|
||||
class TrackAdapter(val fragment: TrackFragment) : RecyclerView.Adapter<TrackHolder>() {
|
||||
|
||||
var items = emptyList<TrackItem>()
|
||||
set(value) {
|
||||
if (field !== value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
var onClickListener: (TrackItem) -> Unit = {}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder {
|
||||
val view = parent.inflate(R.layout.item_track)
|
||||
return TrackHolder(view, fragment)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: TrackHolder, position: Int) {
|
||||
holder.onSetValues(items[position])
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.NumberPicker
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import kotlinx.android.synthetic.main.fragment_track.*
|
||||
import nucleus.factory.RequiresPresenter
|
||||
|
||||
@RequiresPresenter(TrackPresenter::class)
|
||||
class TrackFragment : BaseRxFragment<TrackPresenter>() {
|
||||
|
||||
companion object {
|
||||
fun newInstance(): TrackFragment {
|
||||
return TrackFragment()
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var adapter: TrackAdapter
|
||||
|
||||
private var dialog: TrackSearchDialog? = null
|
||||
|
||||
private val searchFragmentTag: String
|
||||
get() = "search_fragment"
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View {
|
||||
return inflater.inflate(R.layout.fragment_track, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
adapter = TrackAdapter(this)
|
||||
recycler.layoutManager = LinearLayoutManager(context)
|
||||
recycler.adapter = adapter
|
||||
swipe_refresh.isEnabled = false
|
||||
swipe_refresh.setOnRefreshListener { presenter.refresh() }
|
||||
}
|
||||
|
||||
private fun findSearchFragmentIfNeeded() {
|
||||
if (dialog == null) {
|
||||
dialog = childFragmentManager.findFragmentByTag(searchFragmentTag) as TrackSearchDialog
|
||||
}
|
||||
}
|
||||
|
||||
fun onNextTrackings(trackings: List<TrackItem>) {
|
||||
adapter.items = trackings
|
||||
swipe_refresh.isEnabled = trackings.any { it.track != null }
|
||||
(activity as MangaActivity).setTrackingIcon(trackings.any { it.track != null })
|
||||
}
|
||||
|
||||
fun onSearchResults(results: List<Track>) {
|
||||
if (!isResumed) return
|
||||
|
||||
findSearchFragmentIfNeeded()
|
||||
dialog?.onSearchResults(results)
|
||||
}
|
||||
|
||||
fun onSearchResultsError(error: Throwable) {
|
||||
if (!isResumed) return
|
||||
|
||||
findSearchFragmentIfNeeded()
|
||||
dialog?.onSearchResultsError()
|
||||
}
|
||||
|
||||
fun onRefreshDone() {
|
||||
swipe_refresh.isRefreshing = false
|
||||
}
|
||||
|
||||
fun onRefreshError(error: Throwable) {
|
||||
swipe_refresh.isRefreshing = false
|
||||
context.toast(error.message)
|
||||
}
|
||||
|
||||
fun onTitleClick(item: TrackItem) {
|
||||
if (!isResumed) return
|
||||
|
||||
if (dialog == null) {
|
||||
dialog = TrackSearchDialog.newInstance()
|
||||
}
|
||||
|
||||
presenter.selectedService = item.service
|
||||
dialog?.show(childFragmentManager, searchFragmentTag)
|
||||
}
|
||||
|
||||
fun onStatusClick(item: TrackItem) {
|
||||
if (!isResumed || item.track == null) return
|
||||
|
||||
val statusList = item.service.getStatusList().map { item.service.getStatus(it) }
|
||||
val selectedIndex = item.service.getStatusList().indexOf(item.track.status)
|
||||
|
||||
MaterialDialog.Builder(context)
|
||||
.title(R.string.status)
|
||||
.items(statusList)
|
||||
.itemsCallbackSingleChoice(selectedIndex, { dialog, view, i, charSequence ->
|
||||
presenter.setStatus(item, i)
|
||||
swipe_refresh.isRefreshing = true
|
||||
true
|
||||
})
|
||||
.show()
|
||||
}
|
||||
|
||||
fun onChaptersClick(item: TrackItem) {
|
||||
if (!isResumed || item.track == null) return
|
||||
|
||||
val dialog = MaterialDialog.Builder(context)
|
||||
.title(R.string.chapters)
|
||||
.customView(R.layout.dialog_track_chapters, false)
|
||||
.positiveText(android.R.string.ok)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.onPositive { d, action ->
|
||||
val view = d.customView
|
||||
if (view != null) {
|
||||
val np = view.findViewById(R.id.chapters_picker) as NumberPicker
|
||||
np.clearFocus()
|
||||
presenter.setLastChapterRead(item, np.value)
|
||||
swipe_refresh.isRefreshing = true
|
||||
}
|
||||
}
|
||||
.show()
|
||||
|
||||
val view = dialog.customView
|
||||
if (view != null) {
|
||||
val np = view.findViewById(R.id.chapters_picker) as NumberPicker
|
||||
// Set initial value
|
||||
np.value = item.track.last_chapter_read
|
||||
// Don't allow to go from 0 to 9999
|
||||
np.wrapSelectorWheel = false
|
||||
}
|
||||
}
|
||||
|
||||
fun onScoreClick(item: TrackItem) {
|
||||
if (!isResumed || item.track == null) return
|
||||
|
||||
val dialog = MaterialDialog.Builder(activity)
|
||||
.title(R.string.score)
|
||||
.customView(R.layout.dialog_track_score, false)
|
||||
.positiveText(android.R.string.ok)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.onPositive { d, action ->
|
||||
val view = d.customView
|
||||
if (view != null) {
|
||||
val np = view.findViewById(R.id.score_picker) as NumberPicker
|
||||
np.clearFocus()
|
||||
presenter.setScore(item, np.value)
|
||||
swipe_refresh.isRefreshing = true
|
||||
}
|
||||
}
|
||||
.show()
|
||||
|
||||
val view = dialog.customView
|
||||
if (view != null) {
|
||||
val np = view.findViewById(R.id.score_picker) as NumberPicker
|
||||
np.maxValue = item.service.maxScore()
|
||||
// Set initial value
|
||||
np.value = item.track.score.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import kotlinx.android.synthetic.main.item_track.view.*
|
||||
|
||||
class TrackHolder(private val view: View, private val fragment: TrackFragment)
|
||||
: RecyclerView.ViewHolder(view) {
|
||||
|
||||
private lateinit var item: TrackItem
|
||||
|
||||
init {
|
||||
view.title_container.setOnClickListener { fragment.onTitleClick(item) }
|
||||
view.status_container.setOnClickListener { fragment.onStatusClick(item) }
|
||||
view.chapters_container.setOnClickListener { fragment.onChaptersClick(item) }
|
||||
view.score_container.setOnClickListener { fragment.onScoreClick(item) }
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun onSetValues(item: TrackItem) = with(view) {
|
||||
this@TrackHolder.item = item
|
||||
val track = item.track
|
||||
track_logo.setImageResource(item.service.getLogo())
|
||||
logo.setBackgroundColor(item.service.getLogoColor())
|
||||
if (track != null) {
|
||||
track_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary)
|
||||
track_title.setAllCaps(false)
|
||||
track_title.text = track.title
|
||||
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)
|
||||
} else {
|
||||
track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button)
|
||||
track_title.setText(R.string.action_edit)
|
||||
track_chapters.text = ""
|
||||
track_score.text = ""
|
||||
track_status.text = ""
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
|
||||
class TrackItem(val track: Track?, val service: TrackService) {
|
||||
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
||||
import eu.kanade.tachiyomi.util.SharedData
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class TrackPresenter : BasePresenter<TrackFragment>() {
|
||||
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
|
||||
lateinit var manga: Manga
|
||||
private set
|
||||
|
||||
private var trackList: List<TrackItem> = emptyList()
|
||||
|
||||
private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||
|
||||
var selectedService: TrackService? = null
|
||||
|
||||
private var trackSubscription: Subscription? = null
|
||||
|
||||
private var searchSubscription: Subscription? = null
|
||||
|
||||
private var refreshSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
|
||||
fetchTrackings()
|
||||
}
|
||||
|
||||
fun fetchTrackings() {
|
||||
trackSubscription?.let { remove(it) }
|
||||
trackSubscription = db.getTracks(manga)
|
||||
.asRxObservable()
|
||||
.map { tracks ->
|
||||
loggedServices.map { service ->
|
||||
TrackItem(tracks.find { it.sync_id == service.id }, service)
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { trackList = it }
|
||||
.subscribeLatestCache(TrackFragment::onNextTrackings)
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
refreshSubscription?.let { remove(it) }
|
||||
refreshSubscription = Observable.from(trackList)
|
||||
.filter { it.track != null }
|
||||
.concatMap { item ->
|
||||
item.service.refresh(item.track!!)
|
||||
.flatMap { db.insertTrack(it).asRxObservable() }
|
||||
.map { item }
|
||||
.onErrorReturn { item }
|
||||
}
|
||||
.toList()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, result -> view.onRefreshDone() },
|
||||
TrackFragment::onRefreshError)
|
||||
}
|
||||
|
||||
fun search(query: String) {
|
||||
val service = selectedService ?: return
|
||||
|
||||
searchSubscription?.let { remove(it) }
|
||||
searchSubscription = service.search(query)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(TrackFragment::onSearchResults,
|
||||
TrackFragment::onSearchResultsError)
|
||||
}
|
||||
|
||||
fun registerTracking(item: Track?) {
|
||||
val service = selectedService ?: return
|
||||
|
||||
if (item != null) {
|
||||
item.manga_id = manga.id!!
|
||||
add(service.bind(item)
|
||||
.flatMap { db.insertTrack(item).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ },
|
||||
{ error -> context.toast(error.message) }))
|
||||
} else {
|
||||
db.deleteTrackForManga(manga, service).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRemote(track: Track, service: TrackService) {
|
||||
service.update(track)
|
||||
.flatMap { db.insertTrack(track).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, result -> view.onRefreshDone() },
|
||||
{ view, error ->
|
||||
view.onRefreshError(error)
|
||||
|
||||
// Restart on error to set old values
|
||||
fetchTrackings()
|
||||
})
|
||||
}
|
||||
|
||||
fun setStatus(item: TrackItem, index: Int) {
|
||||
val track = item.track!!
|
||||
track.status = item.service.getStatusList()[index]
|
||||
updateRemote(track, item.service)
|
||||
}
|
||||
|
||||
fun setScore(item: TrackItem, score: Int) {
|
||||
val track = item.track!!
|
||||
track.score = score.toFloat()
|
||||
updateRemote(track, item.service)
|
||||
}
|
||||
|
||||
fun setLastChapterRead(item: TrackItem, chapterNumber: Int) {
|
||||
val track = item.track!!
|
||||
track.last_chapter_read = chapterNumber
|
||||
updateRemote(track, item.service)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import kotlinx.android.synthetic.main.item_track_search.view.*
|
||||
import java.util.*
|
||||
|
||||
class TrackSearchAdapter(context: Context)
|
||||
: ArrayAdapter<Track>(context, R.layout.item_track_search, ArrayList<Track>()) {
|
||||
|
||||
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
|
||||
var v = view
|
||||
// Get the data item for this position
|
||||
val track = getItem(position)
|
||||
// Check if an existing view is being reused, otherwise inflate the view
|
||||
val holder: TrackSearchHolder // view lookup cache stored in tag
|
||||
if (v == null) {
|
||||
v = parent.inflate(R.layout.item_track_search)
|
||||
holder = TrackSearchHolder(v)
|
||||
v.tag = holder
|
||||
} else {
|
||||
holder = v.tag as TrackSearchHolder
|
||||
}
|
||||
holder.onSetValues(track)
|
||||
return v
|
||||
}
|
||||
|
||||
fun setItems(syncs: List<Track>) {
|
||||
setNotifyOnChange(false)
|
||||
clear()
|
||||
addAll(syncs)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
class TrackSearchHolder(private val view: View) {
|
||||
|
||||
fun onSetValues(track: Track) {
|
||||
view.track_search_title.text = track.title
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.view.View
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
|
||||
import kotlinx.android.synthetic.main.dialog_track_search.view.*
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class TrackSearchDialog : DialogFragment() {
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(): TrackSearchDialog {
|
||||
return TrackSearchDialog()
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var v: View
|
||||
|
||||
lateinit var adapter: TrackSearchAdapter
|
||||
private set
|
||||
|
||||
private val queryRelay by lazy { PublishRelay.create<String>() }
|
||||
|
||||
private var searchDebounceSubscription: Subscription? = null
|
||||
|
||||
private var selectedItem: Track? = null
|
||||
|
||||
val presenter: TrackPresenter
|
||||
get() = (parentFragment as TrackFragment).presenter
|
||||
|
||||
override fun onCreateDialog(savedState: Bundle?): Dialog {
|
||||
val dialog = MaterialDialog.Builder(context)
|
||||
.customView(R.layout.dialog_track_search, false)
|
||||
.positiveText(android.R.string.ok)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.onPositive { dialog1, which -> onPositiveButtonClick() }
|
||||
.build()
|
||||
|
||||
onViewCreated(dialog.view, savedState)
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||
v = view
|
||||
|
||||
// Create adapter
|
||||
adapter = TrackSearchAdapter(context)
|
||||
view.track_search_list.adapter = adapter
|
||||
|
||||
// Set listeners
|
||||
selectedItem = null
|
||||
view.track_search_list.setOnItemClickListener { parent, viewList, position, id ->
|
||||
selectedItem = adapter.getItem(position)
|
||||
}
|
||||
|
||||
// Do an initial search based on the manga's title
|
||||
if (savedState == null) {
|
||||
val title = presenter.manga.title
|
||||
view.track_search.append(title)
|
||||
search(title)
|
||||
}
|
||||
|
||||
view.track_search.addTextChangedListener(object : SimpleTextWatcher() {
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
queryRelay.call(s.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Listen to text changes
|
||||
searchDebounceSubscription = queryRelay.debounce(1, TimeUnit.SECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.filter { it.isNotBlank() }
|
||||
.subscribe { search(it) }
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
searchDebounceSubscription?.unsubscribe()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun search(query: String) {
|
||||
v.progress.visibility = View.VISIBLE
|
||||
v.track_search_list.visibility = View.GONE
|
||||
|
||||
presenter.search(query)
|
||||
}
|
||||
|
||||
fun onSearchResults(results: List<Track>) {
|
||||
selectedItem = null
|
||||
v.progress.visibility = View.GONE
|
||||
v.track_search_list.visibility = View.VISIBLE
|
||||
adapter.setItems(results)
|
||||
}
|
||||
|
||||
fun onSearchResultsError() {
|
||||
v.progress.visibility = View.VISIBLE
|
||||
v.track_search_list.visibility = View.GONE
|
||||
adapter.setItems(emptyList())
|
||||
}
|
||||
|
||||
private fun onPositiveButtonClick() {
|
||||
presenter.registerTracking(selectedItem)
|
||||
}
|
||||
|
||||
}
|
@ -1,89 +1,94 @@
|
||||
package eu.kanade.tachiyomi.ui.setting
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v7.preference.PreferenceCategory
|
||||
import android.support.v7.preference.XpPreferenceFragment
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.widget.preference.LoginPreference
|
||||
import eu.kanade.tachiyomi.widget.preference.MangaSyncLoginDialog
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class SettingsSyncFragment : SettingsFragment() {
|
||||
|
||||
companion object {
|
||||
const val SYNC_CHANGE_REQUEST = 121
|
||||
|
||||
fun newInstance(rootKey: String): SettingsSyncFragment {
|
||||
val args = Bundle()
|
||||
args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey)
|
||||
return SettingsSyncFragment().apply { arguments = args }
|
||||
}
|
||||
}
|
||||
|
||||
private val syncManager: MangaSyncManager by injectLazy()
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
val syncCategory: PreferenceCategory by bindPref(R.string.pref_category_manga_sync_accounts_key)
|
||||
|
||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||
super.onViewCreated(view, savedState)
|
||||
|
||||
registerService(syncManager.myAnimeList)
|
||||
|
||||
// registerService(syncManager.aniList) {
|
||||
// val intent = CustomTabsIntent.Builder()
|
||||
// .setToolbarColor(activity.theme.getResourceColor(R.attr.colorPrimary))
|
||||
// .build()
|
||||
// intent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||
// intent.launchUrl(activity, AnilistApi.authUrl())
|
||||
// }
|
||||
}
|
||||
|
||||
private fun <T : MangaSyncService> registerService(
|
||||
service: T,
|
||||
onPreferenceClick: (T) -> Unit = defaultOnPreferenceClick) {
|
||||
|
||||
LoginPreference(preferenceManager.context).apply {
|
||||
key = preferences.keys.syncUsername(service.id)
|
||||
title = service.name
|
||||
|
||||
setOnPreferenceClickListener {
|
||||
onPreferenceClick(service)
|
||||
true
|
||||
}
|
||||
|
||||
syncCategory.addPreference(this)
|
||||
}
|
||||
}
|
||||
|
||||
private val defaultOnPreferenceClick: (MangaSyncService) -> Unit
|
||||
get() = {
|
||||
val fragment = MangaSyncLoginDialog.newInstance(it)
|
||||
fragment.setTargetFragment(this, SYNC_CHANGE_REQUEST)
|
||||
fragment.show(fragmentManager, null)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// Manually refresh anilist holder
|
||||
// updatePreference(syncManager.aniList.id)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == SYNC_CHANGE_REQUEST) {
|
||||
updatePreference(resultCode)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePreference(id: Int) {
|
||||
val pref = findPreference(preferences.keys.syncUsername(id)) as? LoginPreference
|
||||
pref?.notifyChanged()
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.setting
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.customtabs.CustomTabsIntent
|
||||
import android.support.v7.preference.PreferenceCategory
|
||||
import android.support.v7.preference.XpPreferenceFragment
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import eu.kanade.tachiyomi.widget.preference.LoginPreference
|
||||
import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class SettingsTrackingFragment : SettingsFragment() {
|
||||
|
||||
companion object {
|
||||
const val SYNC_CHANGE_REQUEST = 121
|
||||
|
||||
fun newInstance(rootKey: String): SettingsTrackingFragment {
|
||||
val args = Bundle()
|
||||
args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey)
|
||||
return SettingsTrackingFragment().apply { arguments = args }
|
||||
}
|
||||
}
|
||||
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
val syncCategory: PreferenceCategory by bindPref(R.string.pref_category_tracking_accounts_key)
|
||||
|
||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||
super.onViewCreated(view, savedState)
|
||||
|
||||
registerService(trackManager.myAnimeList)
|
||||
|
||||
registerService(trackManager.aniList) {
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setToolbarColor(activity.theme.getResourceColor(R.attr.colorPrimary))
|
||||
.build()
|
||||
intent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||
intent.launchUrl(activity, AnilistApi.authUrl())
|
||||
}
|
||||
|
||||
registerService(trackManager.kitsu)
|
||||
}
|
||||
|
||||
private fun <T : TrackService> registerService(
|
||||
service: T,
|
||||
onPreferenceClick: (T) -> Unit = defaultOnPreferenceClick) {
|
||||
|
||||
LoginPreference(preferenceManager.context).apply {
|
||||
key = preferences.keys.trackUsername(service.id)
|
||||
title = service.name
|
||||
|
||||
setOnPreferenceClickListener {
|
||||
onPreferenceClick(service)
|
||||
true
|
||||
}
|
||||
|
||||
syncCategory.addPreference(this)
|
||||
}
|
||||
}
|
||||
|
||||
private val defaultOnPreferenceClick: (TrackService) -> Unit
|
||||
get() = {
|
||||
val fragment = TrackLoginDialog.newInstance(it)
|
||||
fragment.setTargetFragment(this, SYNC_CHANGE_REQUEST)
|
||||
fragment.show(fragmentManager, null)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// Manually refresh anilist holder
|
||||
updatePreference(trackManager.aniList.id)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == SYNC_CHANGE_REQUEST) {
|
||||
updatePreference(resultCode)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePreference(id: Int) {
|
||||
val pref = findPreference(preferences.keys.trackUsername(id)) as? LoginPreference
|
||||
pref?.notifyChanged()
|
||||
}
|
||||
|
||||
}
|
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 2.2 KiB |
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="18dp"
|
||||
android:height="18dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
|
||||
</vector>
|
@ -1,149 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.v7.widget.CardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/cv_mal"
|
||||
style="@style/Theme.Widget.CardView"
|
||||
>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/card_margin">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/myanimelist_title_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:background="?attr/selectable_list_drawable"
|
||||
android:clickable="true"
|
||||
android:paddingLeft="?android:listPreferredItemPaddingLeft"
|
||||
android:paddingRight="?android:listPreferredItemPaddingRight">
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.Regular.Body1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:text="Title"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/myanimelist_title"
|
||||
style="@style/TextAppearance.Medium.Button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:text="@string/action_edit"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider1"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_below="@id/myanimelist_title_layout"
|
||||
android:background="?android:attr/divider"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/myanimelist_status_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:layout_below="@id/divider1"
|
||||
android:background="?attr/selectable_list_drawable"
|
||||
android:clickable="true"
|
||||
android:paddingLeft="?android:listPreferredItemPaddingLeft"
|
||||
android:paddingRight="?android:listPreferredItemPaddingRight"
|
||||
>
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.Regular.Body1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:text="Status"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/myanimelist_status"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
tools:text="Reading"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider2"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_below="@id/myanimelist_status_layout"
|
||||
android:background="?android:attr/divider"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/myanimelist_chapters_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:layout_below="@id/divider2"
|
||||
android:background="?attr/selectable_list_drawable"
|
||||
android:clickable="true"
|
||||
android:paddingLeft="?android:listPreferredItemPaddingLeft"
|
||||
android:paddingRight="?android:listPreferredItemPaddingRight">
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.Regular.Body1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:text="Chapters"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/myanimelist_chapters"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
tools:text="12/24"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider3"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_below="@id/myanimelist_chapters_layout"
|
||||
android:background="?android:attr/divider"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/myanimelist_score_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:layout_below="@id/divider3"
|
||||
android:background="?attr/selectable_list_drawable"
|
||||
android:clickable="true"
|
||||
android:paddingLeft="?android:listPreferredItemPaddingLeft"
|
||||
android:paddingRight="?android:listPreferredItemPaddingRight">
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.Regular.Body1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:text="@string/score"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/myanimelist_score"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
tools:text="10"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</android.support.v7.widget.CardView>
|
@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="?attr/selectable_list_drawable">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/myanimelist_result_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="10dp"/>
|
||||
|
||||
</LinearLayout>
|
@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.v4.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/card_myanimelist_personal"/>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</android.support.v4.widget.SwipeRefreshLayout>
|
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<android.support.v4.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
</android.support.v4.widget.SwipeRefreshLayout>
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,185 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.v7.widget.CardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/track"
|
||||
style="@style/Theme.Widget.CardView">
|
||||
|
||||
<android.support.constraint.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/logo"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="0dp"
|
||||
tools:background="#2E51A2"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/track_logo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
tools:src="@drawable/mal" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/title_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:background="?attr/selectable_list_drawable"
|
||||
android:clickable="true"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@+id/logo"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.Regular.Body1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/track_title"
|
||||
style="@style/TextAppearance.Medium.Button"
|
||||
android:textColor="?colorAccent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:text="@string/action_edit" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:background="?android:attr/divider"
|
||||
app:layout_constraintTop_toBottomOf="@+id/title_container"
|
||||
app:layout_constraintLeft_toRightOf="@+id/logo"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/status_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:background="?attr/selectable_list_drawable"
|
||||
android:clickable="true"
|
||||
app:layout_constraintTop_toBottomOf="@+id/divider1"
|
||||
app:layout_constraintLeft_toRightOf="@+id/logo"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.Regular.Body1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/status" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/track_status"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
tools:text="Reading" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:background="?android:attr/divider"
|
||||
app:layout_constraintTop_toBottomOf="@+id/status_container"
|
||||
app:layout_constraintLeft_toRightOf="@+id/logo"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/chapters_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:background="?attr/selectable_list_drawable"
|
||||
android:clickable="true"
|
||||
app:layout_constraintTop_toBottomOf="@+id/divider2"
|
||||
app:layout_constraintLeft_toRightOf="@+id/logo"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.Regular.Body1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/chapters" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/track_chapters"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
tools:text="12/24" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:background="?android:attr/divider"
|
||||
app:layout_constraintTop_toBottomOf="@+id/chapters_container"
|
||||
app:layout_constraintLeft_toRightOf="@+id/logo"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/score_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:background="?attr/selectable_list_drawable"
|
||||
android:clickable="true"
|
||||
app:layout_constraintTop_toBottomOf="@+id/divider3"
|
||||
app:layout_constraintLeft_toRightOf="@+id/logo"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.Regular.Body1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/score" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/track_score"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
tools:text="10" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
|
||||
</android.support.v7.widget.CardView>
|
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="?attr/selectable_list_drawable">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/track_search_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="10dp"/>
|
||||
|
||||
</LinearLayout>
|
@ -1,30 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<PreferenceScreen
|
||||
android:icon="@drawable/ic_sync_black_24dp"
|
||||
android:key="sync_screen"
|
||||
android:key="tracking_screen"
|
||||
android:persistent="false"
|
||||
android:title="@string/pref_category_sync"
|
||||
android:title="@string/pref_category_tracking"
|
||||
app:asp_tintEnabled="true">
|
||||
|
||||
<SwitchPreference
|
||||
android:key="@string/pref_auto_update_manga_sync_key"
|
||||
android:title="@string/pref_auto_update_manga_sync"
|
||||
android:defaultValue="true" />
|
||||
android:defaultValue="true"
|
||||
app:showText="false"/>
|
||||
|
||||
<SwitchPreference
|
||||
android:key="@string/pref_ask_update_manga_sync_key"
|
||||
android:title="@string/pref_ask_update_manga_sync"
|
||||
android:defaultValue="false"
|
||||
android:dependency="@string/pref_auto_update_manga_sync_key" />
|
||||
android:dependency="@string/pref_auto_update_manga_sync_key"
|
||||
app:showText="false"/>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="@string/pref_category_manga_sync_accounts_key"
|
||||
android:key="@string/pref_category_tracking_accounts_key"
|
||||
android:title="@string/services"
|
||||
android:persistent="false" />
|
||||
android:persistent="false"
|
||||
app:showText="false"/>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
Loading…
Reference in new issue