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?
This commit is contained in:
inorichi 2016-12-18 22:56:28 +01:00 committed by GitHub
parent e3d430eb5e
commit 94ee4e7fb5
76 changed files with 2310 additions and 1654 deletions

View File

@ -12,11 +12,21 @@ android:
- extra-android-support - extra-android-support
- extra-google-google_play_services - extra-google-google_play_services
licenses:
- android-sdk-license-.+
- '.+'
jdk: jdk:
- oraclejdk8 - oraclejdk8
before_script: before_script:
- chmod +x gradlew - chmod +x gradlew
before_install:
- mkdir "$ANDROID_HOME/licenses" || true
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license"
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
#Build, and run tests #Build, and run tests
script: "./gradlew clean buildStandardDebug" script: "./gradlew clean buildStandardDebug"
sudo: false sudo: false

View File

@ -110,6 +110,8 @@ dependencies {
compile "com.android.support:support-annotations:$support_library_version" compile "com.android.support:support-annotations:$support_library_version"
compile "com.android.support:customtabs:$support_library_version" compile "com.android.support:customtabs:$support_library_version"
compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4'
compile 'com.android.support:multidex:1.0.1' compile 'com.android.support:multidex:1.0.1'
// ReactiveX // ReactiveX

View File

@ -53,6 +53,18 @@
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/FilePickerTheme"> android:theme="@style/FilePickerTheme">
</activity> </activity>
<activity
android:name=".ui.setting.AnilistLoginActivity"
android:label="Anilist">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="anilist-auth"
android:scheme="tachiyomi" />
</intent-filter>
</activity>
<provider <provider
android:name="android.support.v4.content.FileProvider" android:name="android.support.v4.content.FileProvider"
@ -70,7 +82,7 @@
<service android:name=".data.download.DownloadService" <service android:name=".data.download.DownloadService"
android:exported="false"/> android:exported="false"/>
<service android:name=".data.mangasync.UpdateMangaSyncService" <service android:name=".data.track.TrackUpdateService"
android:exported="false"/> android:exported="false"/>
<service android:name=".data.updater.UpdateDownloaderService" <service android:name=".data.updater.UpdateDownloaderService"

View File

@ -20,7 +20,7 @@ import uy.kohesive.injekt.registry.default.DefaultRegistrar
reportType = org.acra.sender.HttpSender.Type.JSON, reportType = org.acra.sender.HttpSender.Type.JSON,
httpMethod = org.acra.sender.HttpSender.Method.PUT, httpMethod = org.acra.sender.HttpSender.Method.PUT,
buildConfigClass = BuildConfig::class, buildConfigClass = BuildConfig::class,
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*") excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*", ".*token.*")
) )
open class App : Application() { open class App : Application() {

View File

@ -6,7 +6,7 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.network.NetworkHelper import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
@ -32,7 +32,7 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { DownloadManager(app) } addSingletonFactory { DownloadManager(app) }
addSingletonFactory { MangaSyncManager(app) } addSingletonFactory { TrackManager(app) }
addSingletonFactory { Gson() } addSingletonFactory { Gson() }

View File

@ -39,7 +39,7 @@ class BackupManager(private val db: DatabaseHelper) {
private val MANGA = "manga" private val MANGA = "manga"
private val MANGAS = "mangas" private val MANGAS = "mangas"
private val CHAPTERS = "chapters" private val CHAPTERS = "chapters"
private val MANGA_SYNC = "sync" private val TRACK = "sync"
private val CATEGORIES = "categories" private val CATEGORIES = "categories"
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
@ -109,10 +109,10 @@ class BackupManager(private val db: DatabaseHelper) {
entry.add(CHAPTERS, gson.toJsonTree(chapters)) entry.add(CHAPTERS, gson.toJsonTree(chapters))
} }
// Backup manga sync // Backup tracks
val mangaSync = db.getMangasSync(manga).executeAsBlocking() val tracks = db.getTracks(manga).executeAsBlocking()
if (!mangaSync.isEmpty()) { if (!tracks.isEmpty()) {
entry.add(MANGA_SYNC, gson.toJsonTree(mangaSync)) entry.add(TRACK, gson.toJsonTree(tracks))
} }
// Backup categories for this manga // Backup categories for this manga
@ -231,13 +231,13 @@ class BackupManager(private val db: DatabaseHelper) {
val element = backupManga.asJsonObject val element = backupManga.asJsonObject
val manga = gson.fromJson(element.get(MANGA), MangaImpl::class.java) val manga = gson.fromJson(element.get(MANGA), MangaImpl::class.java)
val chapters = gson.fromJson<List<ChapterImpl>>(element.get(CHAPTERS) ?: JsonArray()) val chapters = gson.fromJson<List<ChapterImpl>>(element.get(CHAPTERS) ?: JsonArray())
val sync = gson.fromJson<List<MangaSyncImpl>>(element.get(MANGA_SYNC) ?: JsonArray()) val tracks = gson.fromJson<List<TrackImpl>>(element.get(TRACK) ?: JsonArray())
val categories = gson.fromJson<List<String>>(element.get(CATEGORIES) ?: JsonArray()) val categories = gson.fromJson<List<String>>(element.get(CATEGORIES) ?: JsonArray())
// Restore everything related to this manga // Restore everything related to this manga
restoreManga(manga) restoreManga(manga)
restoreChaptersForManga(manga, chapters) restoreChaptersForManga(manga, chapters)
restoreSyncForManga(manga, sync) restoreSyncForManga(manga, tracks)
restoreCategoriesForManga(manga, categories) restoreCategoriesForManga(manga, categories)
} }
} }
@ -333,35 +333,35 @@ class BackupManager(private val db: DatabaseHelper) {
* Restores the sync of a manga. * Restores the sync of a manga.
* *
* @param manga the manga whose sync have to be restored. * @param manga the manga whose sync have to be restored.
* @param sync the sync to restore. * @param tracks the track list to restore.
*/ */
private fun restoreSyncForManga(manga: Manga, sync: List<MangaSync>) { private fun restoreSyncForManga(manga: Manga, tracks: List<Track>) {
// Fix foreign keys with the current manga id // Fix foreign keys with the current manga id
for (mangaSync in sync) { for (track in tracks) {
mangaSync.manga_id = manga.id!! track.manga_id = manga.id!!
} }
val dbSyncs = db.getMangasSync(manga).executeAsBlocking() val dbTracks = db.getTracks(manga).executeAsBlocking()
val syncToUpdate = ArrayList<MangaSync>() val trackToUpdate = ArrayList<Track>()
for (backupSync in sync) { for (backupTrack in tracks) {
// Try to find existing chapter in db // Try to find existing chapter in db
val pos = dbSyncs.indexOf(backupSync) val pos = dbTracks.indexOf(backupTrack)
if (pos != -1) { if (pos != -1) {
// The sync is already in the db, only update its fields // The sync is already in the db, only update its fields
val dbSync = dbSyncs[pos] val dbSync = dbTracks[pos]
// Mark the max chapter as read and nothing else // Mark the max chapter as read and nothing else
dbSync.last_chapter_read = Math.max(backupSync.last_chapter_read, dbSync.last_chapter_read) dbSync.last_chapter_read = Math.max(backupTrack.last_chapter_read, dbSync.last_chapter_read)
syncToUpdate.add(dbSync) trackToUpdate.add(dbSync)
} else { } else {
// Insert new sync. Let the db assign the id // Insert new sync. Let the db assign the id
backupSync.id = null backupTrack.id = null
syncToUpdate.add(backupSync) trackToUpdate.add(backupTrack)
} }
} }
// Update database // Update database
if (!syncToUpdate.isEmpty()) { if (!trackToUpdate.isEmpty()) {
db.insertMangasSync(syncToUpdate).executeAsBlocking() db.insertTracks(trackToUpdate).executeAsBlocking()
} }
} }

View File

@ -5,7 +5,7 @@ import com.google.gson.FieldAttributes
import eu.kanade.tachiyomi.data.database.models.CategoryImpl import eu.kanade.tachiyomi.data.database.models.CategoryImpl
import eu.kanade.tachiyomi.data.database.models.ChapterImpl import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.MangaSyncImpl import eu.kanade.tachiyomi.data.database.models.TrackImpl
class IdExclusion : ExclusionStrategy { class IdExclusion : ExclusionStrategy {
@ -17,7 +17,7 @@ class IdExclusion : ExclusionStrategy {
override fun shouldSkipField(f: FieldAttributes) = when (f.declaringClass) { override fun shouldSkipField(f: FieldAttributes) = when (f.declaringClass) {
MangaImpl::class.java -> mangaExclusions.contains(f.name) MangaImpl::class.java -> mangaExclusions.contains(f.name)
ChapterImpl::class.java -> chapterExclusions.contains(f.name) ChapterImpl::class.java -> chapterExclusions.contains(f.name)
MangaSyncImpl::class.java -> syncExclusions.contains(f.name) TrackImpl::class.java -> syncExclusions.contains(f.name)
CategoryImpl::class.java -> categoryExclusions.contains(f.name) CategoryImpl::class.java -> categoryExclusions.contains(f.name)
else -> false else -> false
} }

View File

@ -10,13 +10,13 @@ import eu.kanade.tachiyomi.data.database.queries.*
* This class provides operations to manage the database through its interfaces. * This class provides operations to manage the database through its interfaces.
*/ */
open class DatabaseHelper(context: Context) open class DatabaseHelper(context: Context)
: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries { : MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
override val db = DefaultStorIOSQLite.builder() override val db = DefaultStorIOSQLite.builder()
.sqliteOpenHelper(DbOpenHelper(context)) .sqliteOpenHelper(DbOpenHelper(context))
.addTypeMapping(Manga::class.java, MangaTypeMapping()) .addTypeMapping(Manga::class.java, MangaTypeMapping())
.addTypeMapping(Chapter::class.java, ChapterTypeMapping()) .addTypeMapping(Chapter::class.java, ChapterTypeMapping())
.addTypeMapping(MangaSync::class.java, MangaSyncTypeMapping()) .addTypeMapping(Track::class.java, TrackTypeMapping())
.addTypeMapping(Category::class.java, CategoryTypeMapping()) .addTypeMapping(Category::class.java, CategoryTypeMapping())
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping()) .addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
.addTypeMapping(History::class.java, HistoryTypeMapping()) .addTypeMapping(History::class.java, HistoryTypeMapping())

View File

@ -23,7 +23,7 @@ class DbOpenHelper(context: Context)
override fun onCreate(db: SQLiteDatabase) = with(db) { override fun onCreate(db: SQLiteDatabase) = with(db) {
execSQL(MangaTable.createTableQuery) execSQL(MangaTable.createTableQuery)
execSQL(ChapterTable.createTableQuery) execSQL(ChapterTable.createTableQuery)
execSQL(MangaSyncTable.createTableQuery) execSQL(TrackTable.createTableQuery)
execSQL(CategoryTable.createTableQuery) execSQL(CategoryTable.createTableQuery)
execSQL(MangaCategoryTable.createTableQuery) execSQL(MangaCategoryTable.createTableQuery)
execSQL(HistoryTable.createTableQuery) execSQL(HistoryTable.createTableQuery)

View File

@ -9,38 +9,38 @@ import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.InsertQuery import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.models.MangaSync import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.MangaSyncImpl import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_ID import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_LAST_CHAPTER_READ import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_MANGA_ID import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_REMOTE_ID import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_REMOTE_ID
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_SCORE import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_STATUS import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_SYNC_ID import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_TITLE import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_TOTAL_CHAPTERS import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TOTAL_CHAPTERS
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.TABLE import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
class MangaSyncTypeMapping : SQLiteTypeMapping<MangaSync>( class TrackTypeMapping : SQLiteTypeMapping<Track>(
MangaSyncPutResolver(), TrackPutResolver(),
MangaSyncGetResolver(), TrackGetResolver(),
MangaSyncDeleteResolver() TrackDeleteResolver()
) )
class MangaSyncPutResolver : DefaultPutResolver<MangaSync>() { class TrackPutResolver : DefaultPutResolver<Track>() {
override fun mapToInsertQuery(obj: MangaSync) = InsertQuery.builder() override fun mapToInsertQuery(obj: Track) = InsertQuery.builder()
.table(TABLE) .table(TABLE)
.build() .build()
override fun mapToUpdateQuery(obj: MangaSync) = UpdateQuery.builder() override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
override fun mapToContentValues(obj: MangaSync) = ContentValues(9).apply { override fun mapToContentValues(obj: Track) = ContentValues(9).apply {
put(COL_ID, obj.id) put(COL_ID, obj.id)
put(COL_MANGA_ID, obj.manga_id) put(COL_MANGA_ID, obj.manga_id)
put(COL_SYNC_ID, obj.sync_id) put(COL_SYNC_ID, obj.sync_id)
@ -53,9 +53,9 @@ class MangaSyncPutResolver : DefaultPutResolver<MangaSync>() {
} }
} }
class MangaSyncGetResolver : DefaultGetResolver<MangaSync>() { class TrackGetResolver : DefaultGetResolver<Track>() {
override fun mapFromCursor(cursor: Cursor): MangaSync = MangaSyncImpl().apply { override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID)) id = cursor.getLong(cursor.getColumnIndex(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)) manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID)) sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
@ -68,9 +68,9 @@ class MangaSyncGetResolver : DefaultGetResolver<MangaSync>() {
} }
} }
class MangaSyncDeleteResolver : DefaultDeleteResolver<MangaSync>() { class TrackDeleteResolver : DefaultDeleteResolver<Track>() {
override fun mapToDeleteQuery(obj: MangaSync) = DeleteQuery.builder() override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable import java.io.Serializable
interface MangaSync : Serializable { interface Track : Serializable {
var id: Long? var id: Long?
@ -24,7 +24,7 @@ interface MangaSync : Serializable {
var update: Boolean var update: Boolean
fun copyPersonalFrom(other: MangaSync) { fun copyPersonalFrom(other: Track) {
last_chapter_read = other.last_chapter_read last_chapter_read = other.last_chapter_read
score = other.score score = other.score
status = other.status status = other.status
@ -32,7 +32,7 @@ interface MangaSync : Serializable {
companion object { companion object {
fun create(serviceId: Int): MangaSync = MangaSyncImpl().apply { fun create(serviceId: Int): Track = TrackImpl().apply {
sync_id = serviceId sync_id = serviceId
} }
} }

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models
class MangaSyncImpl : MangaSync { class TrackImpl : Track {
override var id: Long? = null override var id: Long? = null
@ -26,11 +26,11 @@ class MangaSyncImpl : MangaSync {
if (this === other) return true if (this === other) return true
if (other == null || javaClass != other.javaClass) return false if (other == null || javaClass != other.javaClass) return false
val mangaSync = other as MangaSync other as Track
if (manga_id != mangaSync.manga_id) return false if (manga_id != other.manga_id) return false
if (sync_id != mangaSync.sync_id) return false if (sync_id != other.sync_id) return false
return remote_id == mangaSync.remote_id return remote_id == other.remote_id
} }
override fun hashCode(): Int { override fun hashCode(): Int {

View File

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

View File

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

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.database.tables package eu.kanade.tachiyomi.data.database.tables
object MangaSyncTable { object TrackTable {
const val TABLE = "manga_sync" const val TABLE = "manga_sync"

View File

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

View File

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

View File

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

View File

@ -51,9 +51,9 @@ class PreferenceKeys(context: Context) {
val updateOnlyNonCompleted = context.getString(R.string.pref_update_only_non_completed_key) val updateOnlyNonCompleted = context.getString(R.string.pref_update_only_non_completed_key)
val autoUpdateMangaSync = context.getString(R.string.pref_auto_update_manga_sync_key) val autoUpdateTrack = context.getString(R.string.pref_auto_update_manga_sync_key)
val askUpdateMangaSync = context.getString(R.string.pref_ask_update_manga_sync_key) val askUpdateTrack = context.getString(R.string.pref_ask_update_manga_sync_key)
val lastUsedCatalogueSource = context.getString(R.string.pref_last_catalogue_source_key) val lastUsedCatalogueSource = context.getString(R.string.pref_last_catalogue_source_key)
@ -95,9 +95,11 @@ class PreferenceKeys(context: Context) {
fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId" fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId"
fun syncUsername(syncId: Int) = "pref_mangasync_username_$syncId" fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
fun syncPassword(syncId: Int) = "pref_mangasync_password_$syncId" fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
fun trackToken(syncId: Int) = "track_token_$syncId"
val libraryAsList = context.getString(R.string.pref_display_library_as_list) val libraryAsList = context.getString(R.string.pref_display_library_as_list)

View File

@ -7,8 +7,8 @@ import android.preference.PreferenceManager
import com.f2prateek.rx.preferences.Preference import com.f2prateek.rx.preferences.Preference
import com.f2prateek.rx.preferences.RxSharedPreferences import com.f2prateek.rx.preferences.RxSharedPreferences
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
import eu.kanade.tachiyomi.data.source.Source import eu.kanade.tachiyomi.data.source.Source
import eu.kanade.tachiyomi.data.track.TrackService
import java.io.File import java.io.File
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!! fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
@ -70,9 +70,9 @@ class PreferencesHelper(val context: Context) {
fun updateOnlyNonCompleted() = prefs.getBoolean(keys.updateOnlyNonCompleted, false) fun updateOnlyNonCompleted() = prefs.getBoolean(keys.updateOnlyNonCompleted, false)
fun autoUpdateMangaSync() = prefs.getBoolean(keys.autoUpdateMangaSync, true) fun autoUpdateTrack() = prefs.getBoolean(keys.autoUpdateTrack, true)
fun askUpdateMangaSync() = prefs.getBoolean(keys.askUpdateMangaSync, false) fun askUpdateTrack() = prefs.getBoolean(keys.askUpdateTrack, false)
fun lastUsedCatalogueSource() = rxPrefs.getInteger(keys.lastUsedCatalogueSource, -1) fun lastUsedCatalogueSource() = rxPrefs.getInteger(keys.lastUsedCatalogueSource, -1)
@ -95,17 +95,21 @@ class PreferencesHelper(val context: Context) {
.apply() .apply()
} }
fun mangaSyncUsername(sync: MangaSyncService) = prefs.getString(keys.syncUsername(sync.id), "") fun trackUsername(sync: TrackService) = prefs.getString(keys.trackUsername(sync.id), "")
fun mangaSyncPassword(sync: MangaSyncService) = prefs.getString(keys.syncPassword(sync.id), "") fun trackPassword(sync: TrackService) = prefs.getString(keys.trackPassword(sync.id), "")
fun setMangaSyncCredentials(sync: MangaSyncService, username: String, password: String) { fun setTrackCredentials(sync: TrackService, username: String, password: String) {
prefs.edit() prefs.edit()
.putString(keys.syncUsername(sync.id), username) .putString(keys.trackUsername(sync.id), username)
.putString(keys.syncPassword(sync.id), password) .putString(keys.trackPassword(sync.id), password)
.apply() .apply()
} }
fun trackToken(sync: TrackService) = rxPrefs.getString(keys.trackToken(sync.id), "")
fun anilistScoreType() = rxPrefs.getInteger("anilist_score_type", 0)
fun downloadsDirectory() = rxPrefs.getString(keys.downloadsDirectory, defaultDownloadsDir.toString()) fun downloadsDirectory() = rxPrefs.getString(keys.downloadsDirectory, defaultDownloadsDir.toString())
fun downloadThreads() = rxPrefs.getInteger(keys.downloadThreads, 1) fun downloadThreads() = rxPrefs.getInteger(keys.downloadThreads, 1)

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.source.online.english package eu.kanade.tachiyomi.data.source.online.english
import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.EN import eu.kanade.tachiyomi.data.source.EN

View File

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

View File

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

View File

@ -1,20 +1,20 @@
package eu.kanade.tachiyomi.data.mangasync package eu.kanade.tachiyomi.data.track
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.MangaSync import eu.kanade.tachiyomi.data.database.models.Track
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class UpdateMangaSyncService : Service() { class TrackUpdateService : Service() {
val syncManager: MangaSyncManager by injectLazy() val trackManager: TrackManager by injectLazy()
val db: DatabaseHelper by injectLazy() val db: DatabaseHelper by injectLazy()
private lateinit var subscriptions: CompositeSubscription private lateinit var subscriptions: CompositeSubscription
@ -30,9 +30,9 @@ class UpdateMangaSyncService : Service() {
} }
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val manga = intent.getSerializableExtra(EXTRA_MANGASYNC) val track = intent.getSerializableExtra(EXTRA_TRACK)
if (manga != null) { if (track != null) {
updateLastChapterRead(manga as MangaSync, startId) updateLastChapterRead(track as Track, startId)
return Service.START_REDELIVER_INTENT return Service.START_REDELIVER_INTENT
} else { } else {
stopSelf(startId) stopSelf(startId)
@ -44,15 +44,15 @@ class UpdateMangaSyncService : Service() {
return null return null
} }
private fun updateLastChapterRead(mangaSync: MangaSync, startId: Int) { private fun updateLastChapterRead(track: Track, startId: Int) {
val sync = syncManager.getService(mangaSync.sync_id) val sync = trackManager.getService(track.sync_id)
if (sync == null) { if (sync == null) {
stopSelf(startId) stopSelf(startId)
return return
} }
subscriptions.add(Observable.defer { sync.update(mangaSync) } subscriptions.add(Observable.defer { sync.update(track) }
.flatMap { db.insertMangaSync(mangaSync).asRxObservable() } .flatMap { db.insertTrack(track).asRxObservable() }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ stopSelf(startId) }, .subscribe({ stopSelf(startId) },
@ -61,12 +61,12 @@ class UpdateMangaSyncService : Service() {
companion object { companion object {
private val EXTRA_MANGASYNC = "extra_mangasync" private val EXTRA_TRACK = "extra_track"
@JvmStatic @JvmStatic
fun start(context: Context, mangaSync: MangaSync) { fun start(context: Context, track: Track) {
val intent = Intent(context, UpdateMangaSyncService::class.java) val intent = Intent(context, TrackUpdateService::class.java)
intent.putExtra(EXTRA_MANGASYNC, mangaSync) intent.putExtra(EXTRA_TRACK, track)
context.startService(intent) context.startService(intent)
} }
} }

View File

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

View File

@ -1,89 +1,88 @@
package eu.kanade.tachiyomi.data.mangasync.anilist package eu.kanade.tachiyomi.data.track.anilist
import android.net.Uri import android.net.Uri
import com.google.gson.JsonObject import com.google.gson.JsonObject
import eu.kanade.tachiyomi.data.mangasync.anilist.model.ALManga import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.mangasync.anilist.model.ALUserLists import eu.kanade.tachiyomi.data.track.anilist.model.ALManga
import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth import eu.kanade.tachiyomi.data.track.anilist.model.ALUserLists
import eu.kanade.tachiyomi.data.network.POST import eu.kanade.tachiyomi.data.track.anilist.model.OAuth
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.Response import retrofit2.Response
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.* import retrofit2.http.*
import rx.Observable import rx.Observable
interface AnilistApi { interface AnilistApi {
companion object { companion object {
private const val clientId = "tachiyomi-hrtje" private const val clientId = "tachiyomi-hrtje"
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C" private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
private const val clientUrl = "tachiyomi://anilist-auth" private const val clientUrl = "tachiyomi://anilist-auth"
private const val baseUrl = "https://anilist.co/api/" private const val baseUrl = "https://anilist.co/api/"
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon() fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon()
.appendQueryParameter("grant_type", "authorization_code") .appendQueryParameter("grant_type", "authorization_code")
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", clientId)
.appendQueryParameter("redirect_uri", clientUrl) .appendQueryParameter("redirect_uri", clientUrl)
.appendQueryParameter("response_type", "code") .appendQueryParameter("response_type", "code")
.build() .build()
fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token", fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token",
body = FormBody.Builder() body = FormBody.Builder()
.add("grant_type", "refresh_token") .add("grant_type", "refresh_token")
.add("client_id", clientId) .add("client_id", clientId)
.add("client_secret", clientSecret) .add("client_secret", clientSecret)
.add("refresh_token", token) .add("refresh_token", token)
.build()) .build())
fun createService(client: OkHttpClient) = Retrofit.Builder() fun createService(client: OkHttpClient) = Retrofit.Builder()
.baseUrl(baseUrl) .baseUrl(baseUrl)
.client(client) .client(client)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build() .build()
.create(AnilistApi::class.java) .create(AnilistApi::class.java)
} }
@FormUrlEncoded @FormUrlEncoded
@POST("auth/access_token") @POST("auth/access_token")
fun requestAccessToken( fun requestAccessToken(
@Field("code") code: String, @Field("code") code: String,
@Field("grant_type") grant_type: String = "authorization_code", @Field("grant_type") grant_type: String = "authorization_code",
@Field("client_id") client_id: String = clientId, @Field("client_id") client_id: String = clientId,
@Field("client_secret") client_secret: String = clientSecret, @Field("client_secret") client_secret: String = clientSecret,
@Field("redirect_uri") redirect_uri: String = clientUrl) @Field("redirect_uri") redirect_uri: String = clientUrl)
: Observable<OAuth> : Observable<OAuth>
@GET("user") @GET("user")
fun getCurrentUser(): Observable<JsonObject> fun getCurrentUser(): Observable<JsonObject>
@GET("manga/search/{query}") @GET("manga/search/{query}")
fun search(@Path("query") query: String, @Query("page") page: Int): Observable<List<ALManga>> fun search(@Path("query") query: String, @Query("page") page: Int): Observable<List<ALManga>>
@GET("user/{username}/mangalist") @GET("user/{username}/mangalist")
fun getList(@Path("username") username: String): Observable<ALUserLists> fun getList(@Path("username") username: String): Observable<ALUserLists>
@FormUrlEncoded @FormUrlEncoded
@PUT("mangalist") @PUT("mangalist")
fun addManga( fun addManga(
@Field("id") id: Int, @Field("id") id: Int,
@Field("chapters_read") chapters_read: Int, @Field("chapters_read") chapters_read: Int,
@Field("list_status") list_status: String, @Field("list_status") list_status: String)
@Field("score_raw") score_raw: Int) : Observable<Response<ResponseBody>>
: Observable<Response<ResponseBody>>
@FormUrlEncoded
@FormUrlEncoded @PUT("mangalist")
@PUT("mangalist") fun updateManga(
fun updateManga( @Field("id") id: Int,
@Field("id") id: Int, @Field("chapters_read") chapters_read: Int,
@Field("chapters_read") chapters_read: Int, @Field("list_status") list_status: String,
@Field("list_status") list_status: String, @Field("score") score_raw: String)
@Field("score_raw") score_raw: Int) : Observable<Response<ResponseBody>>
: Observable<Response<ResponseBody>>
} }

View File

@ -1,61 +1,61 @@
package eu.kanade.tachiyomi.data.mangasync.anilist package eu.kanade.tachiyomi.data.track.anilist
import com.google.gson.Gson import com.google.gson.Gson
import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth import eu.kanade.tachiyomi.data.track.anilist.model.OAuth
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
class AnilistInterceptor(private var refreshToken: String?) : Interceptor { class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
/** /**
* OAuth object used for authenticated requests. * OAuth object used for authenticated requests.
* *
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute * Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
* before its original expiration date. * before its original expiration date.
*/ */
private var oauth: OAuth? = null private var oauth: OAuth? = null
set(value) { set(value) {
field = value?.copy(expires = value.expires * 1000 - 60 * 1000) field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
} }
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
if (refreshToken.isNullOrEmpty()) { if (refreshToken.isNullOrEmpty()) {
throw Exception("Not authenticated with Anilist") throw Exception("Not authenticated with Anilist")
} }
// Refresh access token if null or expired. // Refresh access token if null or expired.
if (oauth == null || oauth!!.isExpired()) { if (oauth == null || oauth!!.isExpired()) {
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!)) val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
oauth = if (response.isSuccessful) { oauth = if (response.isSuccessful) {
Gson().fromJson(response.body().string(), OAuth::class.java) Gson().fromJson(response.body().string(), OAuth::class.java)
} else { } else {
response.close() response.close()
null null
} }
} }
// Throw on null auth. // Throw on null auth.
if (oauth == null) { if (oauth == null) {
throw Exception("Access token wasn't refreshed") throw Exception("Access token wasn't refreshed")
} }
// Add the authorization header to the original request. // Add the authorization header to the original request.
val authRequest = originalRequest.newBuilder() val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}") .addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.build() .build()
return chain.proceed(authRequest) return chain.proceed(authRequest)
} }
/** /**
* Called when the user authenticates with Anilist for the first time. Sets the refresh token * Called when the user authenticates with Anilist for the first time. Sets the refresh token
* and the oauth object. * and the oauth object.
*/ */
fun setAuth(oauth: OAuth?) { fun setAuth(oauth: OAuth?) {
refreshToken = oauth?.refresh_token refreshToken = oauth?.refresh_token
this.oauth = oauth this.oauth = oauth
} }
} }

View File

@ -1,17 +1,17 @@
package eu.kanade.tachiyomi.data.mangasync.anilist.model package eu.kanade.tachiyomi.data.track.anilist.model
import eu.kanade.tachiyomi.data.database.models.MangaSync import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager import eu.kanade.tachiyomi.data.track.TrackManager
data class ALManga( data class ALManga(
val id: Int, val id: Int,
val title_romaji: String, val title_romaji: String,
val type: String, val type: String,
val total_chapters: Int) { val total_chapters: Int) {
fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply { fun toTrack() = Track.create(TrackManager.ANILIST).apply {
remote_id = this@ALManga.id remote_id = this@ALManga.id
title = title_romaji title = title_romaji
total_chapters = this@ALManga.total_chapters total_chapters = this@ALManga.total_chapters
} }
} }

View File

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

View File

@ -1,29 +1,29 @@
package eu.kanade.tachiyomi.data.mangasync.anilist.model package eu.kanade.tachiyomi.data.track.anilist.model
import eu.kanade.tachiyomi.data.database.models.MangaSync import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
data class ALUserManga( data class ALUserManga(
val id: Int, val id: Int,
val list_status: String, val list_status: String,
val score_raw: Int, val score_raw: Int,
val chapters_read: Int, val chapters_read: Int,
val manga: ALManga) { val manga: ALManga) {
fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply { fun toTrack() = Track.create(TrackManager.ANILIST).apply {
remote_id = manga.id remote_id = manga.id
status = getMangaSyncStatus() status = toTrackStatus()
score = score_raw.toFloat() score = score_raw.toFloat()
last_chapter_read = chapters_read last_chapter_read = chapters_read
} }
fun getMangaSyncStatus() = when (list_status) { fun toTrackStatus() = when (list_status) {
"reading" -> Anilist.READING "reading" -> Anilist.READING
"completed" -> Anilist.COMPLETED "completed" -> Anilist.COMPLETED
"on-hold" -> Anilist.ON_HOLD "on-hold" -> Anilist.ON_HOLD
"dropped" -> Anilist.DROPPED "dropped" -> Anilist.DROPPED
"plan to read" -> Anilist.PLAN_TO_READ "plan to read" -> Anilist.PLAN_TO_READ
else -> throw NotImplementedError("Unknown status") else -> throw NotImplementedError("Unknown status")
} }
} }

View File

@ -1,11 +1,11 @@
package eu.kanade.tachiyomi.data.mangasync.anilist.model package eu.kanade.tachiyomi.data.track.anilist.model
data class OAuth( data class OAuth(
val access_token: String, val access_token: String,
val token_type: String, val token_type: String,
val expires: Long, val expires: Long,
val expires_in: Long, val expires_in: Long,
val refresh_token: String?) { val refresh_token: String?) {
fun isExpired() = System.currentTimeMillis() > expires fun isExpired() = System.currentTimeMillis() > expires
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,222 +1,263 @@
package eu.kanade.tachiyomi.data.mangasync.myanimelist package eu.kanade.tachiyomi.data.track.myanimelist
import android.content.Context import android.content.Context
import android.net.Uri import android.graphics.Color
import android.util.Xml import android.net.Uri
import eu.kanade.tachiyomi.R import android.util.Xml
import eu.kanade.tachiyomi.data.database.models.MangaSync import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.network.GET import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.POST import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.network.asObservable import eu.kanade.tachiyomi.data.network.asObservable
import eu.kanade.tachiyomi.util.selectInt import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.util.selectText import eu.kanade.tachiyomi.util.selectInt
import okhttp3.Credentials import eu.kanade.tachiyomi.util.selectText
import okhttp3.FormBody import okhttp3.Credentials
import okhttp3.Headers import okhttp3.FormBody
import okhttp3.RequestBody import okhttp3.Headers
import org.jsoup.Jsoup import okhttp3.RequestBody
import org.xmlpull.v1.XmlSerializer import org.jsoup.Jsoup
import rx.Completable import org.xmlpull.v1.XmlSerializer
import rx.Observable import rx.Completable
import java.io.StringWriter import rx.Observable
import java.io.StringWriter
class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) {
class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
private lateinit var headers: Headers
private lateinit var headers: Headers
companion object {
val BASE_URL = "https://myanimelist.net" companion object {
const val BASE_URL = "https://myanimelist.net"
private val ENTRY_TAG = "entry"
private val CHAPTER_TAG = "chapter" private val ENTRY_TAG = "entry"
private val SCORE_TAG = "score" private val CHAPTER_TAG = "chapter"
private val STATUS_TAG = "status" private val SCORE_TAG = "score"
private val STATUS_TAG = "status"
val READING = 1
val COMPLETED = 2 const val READING = 1
val ON_HOLD = 3 const val COMPLETED = 2
val DROPPED = 4 const val ON_HOLD = 3
val PLAN_TO_READ = 6 const val DROPPED = 4
const val PLAN_TO_READ = 6
val DEFAULT_STATUS = READING
val DEFAULT_SCORE = 0 const val DEFAULT_STATUS = READING
} const val DEFAULT_SCORE = 0
init { const val PREFIX_MY = "my:"
val username = getUsername() }
val password = getPassword()
init {
if (!username.isEmpty() && !password.isEmpty()) { val username = getUsername()
createHeaders(username, password) val password = getPassword()
}
} if (!username.isEmpty() && !password.isEmpty()) {
createHeaders(username, password)
override val name: String }
get() = "MyAnimeList" }
fun getLoginUrl() = Uri.parse(BASE_URL).buildUpon() override val name: String
.appendEncodedPath("api/account/verify_credentials.xml") get() = "MyAnimeList"
.toString()
override fun getLogo() = R.drawable.mal
fun getSearchUrl(query: String) = Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/manga/search.xml") override fun getLogoColor() = Color.rgb(46, 81, 162)
.appendQueryParameter("q", query)
.toString() override fun maxScore() = 10
fun getListUrl(username: String) = Uri.parse(BASE_URL).buildUpon() override fun formatScore(track: Track): String {
.appendPath("malappinfo.php") return track.score.toInt().toString()
.appendQueryParameter("u", username) }
.appendQueryParameter("status", "all")
.appendQueryParameter("type", "manga") fun getLoginUrl() = Uri.parse(BASE_URL).buildUpon()
.toString() .appendEncodedPath("api/account/verify_credentials.xml")
.toString()
fun getUpdateUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/update") fun getSearchUrl(query: String) = Uri.parse(BASE_URL).buildUpon()
.appendPath("${manga.remote_id}.xml") .appendEncodedPath("api/manga/search.xml")
.toString() .appendQueryParameter("q", query)
.toString()
fun getAddUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/add") fun getListUrl(username: String) = Uri.parse(BASE_URL).buildUpon()
.appendPath("${manga.remote_id}.xml") .appendPath("malappinfo.php")
.toString() .appendQueryParameter("u", username)
.appendQueryParameter("status", "all")
override fun login(username: String, password: String): Completable { .appendQueryParameter("type", "manga")
createHeaders(username, password) .toString()
return client.newCall(GET(getLoginUrl(), headers))
.asObservable() fun getUpdateUrl(track: Track) = Uri.parse(BASE_URL).buildUpon()
.doOnNext { it.close() } .appendEncodedPath("api/mangalist/update")
.doOnNext { if (it.code() != 200) throw Exception("Login error") } .appendPath("${track.remote_id}.xml")
.toCompletable() .toString()
}
fun getAddUrl(track: Track) = Uri.parse(BASE_URL).buildUpon()
fun search(query: String): Observable<List<MangaSync>> { .appendEncodedPath("api/mangalist/add")
return client.newCall(GET(getSearchUrl(query), headers)) .appendPath("${track.remote_id}.xml")
.asObservable() .toString()
.map { Jsoup.parse(it.body().string()) }
.flatMap { Observable.from(it.select("entry")) } override fun login(username: String, password: String): Completable {
.filter { it.select("type").text() != "Novel" } createHeaders(username, password)
.map { return client.newCall(GET(getLoginUrl(), headers))
MangaSync.create(id).apply { .asObservable()
title = it.selectText("title")!! .doOnNext { it.close() }
remote_id = it.selectInt("id") .doOnNext { if (it.code() != 200) throw Exception("Login error") }
total_chapters = it.selectInt("chapters") .doOnNext { saveCredentials(username, password) }
} .doOnError { logout() }
} .toCompletable()
.toList() }
}
override fun search(query: String): Observable<List<Track>> {
// MAL doesn't support score with decimals return if (query.startsWith(PREFIX_MY)) {
fun getList(): Observable<List<MangaSync>> { val realQuery = query.substring(PREFIX_MY.length).toLowerCase().trim()
return networkService.forceCacheClient getList()
.newCall(GET(getListUrl(getUsername()), headers)) .flatMap { Observable.from(it) }
.asObservable() .filter { realQuery in it.title.toLowerCase() }
.map { Jsoup.parse(it.body().string()) } .toList()
.flatMap { Observable.from(it.select("manga")) } } else {
.map { client.newCall(GET(getSearchUrl(query), headers))
MangaSync.create(id).apply { .asObservable()
title = it.selectText("series_title")!! .map { Jsoup.parse(it.body().string()) }
remote_id = it.selectInt("series_mangadb_id") .flatMap { Observable.from(it.select("entry")) }
last_chapter_read = it.selectInt("my_read_chapters") .filter { it.select("type").text() != "Novel" }
status = it.selectInt("my_status") .map {
score = it.selectInt("my_score").toFloat() Track.create(id).apply {
total_chapters = it.selectInt("series_chapters") title = it.selectText("title")!!
} remote_id = it.selectInt("id")
} total_chapters = it.selectInt("chapters")
.toList() }
} }
.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 override fun refresh(track: Track): Observable<Track> {
} return getList()
client.newCall(POST(getUpdateUrl(manga), headers, getMangaPostPayload(manga))) .map { myList ->
.asObservable() val remoteTrack = myList.find { it.remote_id == track.remote_id }
.doOnNext { it.close() } if (remoteTrack != null) {
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") } track.copyPersonalFrom(remoteTrack)
.map { manga } track.total_chapters = remoteTrack.total_chapters
} track
} else {
} throw Exception("Could not find manga")
}
override fun add(manga: MangaSync): Observable<MangaSync> { }
return Observable.defer { }
client.newCall(POST(getAddUrl(manga), headers, getMangaPostPayload(manga)))
.asObservable() // MAL doesn't support score with decimals
.doOnNext { it.close() } fun getList(): Observable<List<Track>> {
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") } return networkService.forceCacheClient
.map { manga } .newCall(GET(getListUrl(getUsername()), headers))
} .asObservable()
} .map { Jsoup.parse(it.body().string()) }
.flatMap { Observable.from(it.select("manga")) }
private fun getMangaPostPayload(manga: MangaSync): RequestBody { .map {
val xml = Xml.newSerializer() Track.create(id).apply {
val writer = StringWriter() title = it.selectText("series_title")!!
remote_id = it.selectInt("series_mangadb_id")
with(xml) { last_chapter_read = it.selectInt("my_read_chapters")
setOutput(writer) status = it.selectInt("my_status")
startDocument("UTF-8", false) score = it.selectInt("my_score").toFloat()
startTag("", ENTRY_TAG) total_chapters = it.selectInt("series_chapters")
}
// Last chapter read }
if (manga.last_chapter_read != 0) { .toList()
inTag(CHAPTER_TAG, manga.last_chapter_read.toString()) }
}
// Manga status in the list override fun update(track: Track): Observable<Track> {
inTag(STATUS_TAG, manga.status.toString()) return Observable.defer {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
// Manga score track.status = COMPLETED
inTag(SCORE_TAG, manga.score.toString()) }
client.newCall(POST(getUpdateUrl(track), headers, getMangaPostPayload(track)))
endTag("", ENTRY_TAG) .asObservable()
endDocument() .doOnNext { it.close() }
} .doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
.map { track }
val form = FormBody.Builder() }
form.add("data", writer.toString())
return form.build() }
}
override fun add(track: Track): Observable<Track> {
fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") { return Observable.defer {
startTag(namespace, tag) client.newCall(POST(getAddUrl(track), headers, getMangaPostPayload(track)))
text(body) .asObservable()
endTag(namespace, tag) .doOnNext { it.close() }
} .doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
.map { track }
override fun bind(manga: MangaSync): Observable<MangaSync> { }
return getList() }
.flatMap { userlist ->
manga.sync_id = id private fun getMangaPostPayload(track: Track): RequestBody {
val mangaFromList = userlist.find { it.remote_id == manga.remote_id } val xml = Xml.newSerializer()
if (mangaFromList != null) { val writer = StringWriter()
manga.copyPersonalFrom(mangaFromList)
update(manga) with(xml) {
} else { setOutput(writer)
// Set default fields if it's not found in the list startDocument("UTF-8", false)
manga.score = DEFAULT_SCORE.toFloat() startTag("", ENTRY_TAG)
manga.status = DEFAULT_STATUS
add(manga) // Last chapter read
} if (track.last_chapter_read != 0) {
} inTag(CHAPTER_TAG, track.last_chapter_read.toString())
} }
// Manga status in the list
override fun getStatus(status: Int): String = with(context) { inTag(STATUS_TAG, track.status.toString())
when (status) {
READING -> getString(R.string.reading) // Manga score
COMPLETED -> getString(R.string.completed) inTag(SCORE_TAG, track.score.toString())
ON_HOLD -> getString(R.string.on_hold)
DROPPED -> getString(R.string.dropped) endTag("", ENTRY_TAG)
PLAN_TO_READ -> getString(R.string.plan_to_read) endDocument()
else -> "" }
}
} val form = FormBody.Builder()
form.add("data", writer.toString())
fun createHeaders(username: String, password: String) { return form.build()
val builder = Headers.Builder() }
builder.add("Authorization", Credentials.basic(username, password))
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C") fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
headers = builder.build() 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()
}
}

View File

@ -3,15 +3,18 @@ package eu.kanade.tachiyomi.ui.manga
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.graphics.drawable.VectorDrawableCompat
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager import android.support.v4.app.FragmentManager
import android.support.v4.app.FragmentPagerAdapter import android.support.v4.app.FragmentPagerAdapter
import android.widget.LinearLayout
import android.widget.TextView
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoFragment import eu.kanade.tachiyomi.ui.manga.info.MangaInfoFragment
import eu.kanade.tachiyomi.ui.manga.myanimelist.MyAnimeListFragment import eu.kanade.tachiyomi.ui.manga.track.TrackFragment
import eu.kanade.tachiyomi.util.SharedData import eu.kanade.tachiyomi.util.SharedData
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.activity_manga.* import kotlinx.android.synthetic.main.activity_manga.*
@ -28,7 +31,7 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
const val FROM_LAUNCHER_EXTRA = "from_launcher" const val FROM_LAUNCHER_EXTRA = "from_launcher"
const val INFO_FRAGMENT = 0 const val INFO_FRAGMENT = 0
const val CHAPTERS_FRAGMENT = 1 const val CHAPTERS_FRAGMENT = 1
const val MYANIMELIST_FRAGMENT = 2 const val TRACK_FRAGMENT = 2
fun newIntent(context: Context, manga: Manga, fromCatalogue: Boolean = false): Intent { fun newIntent(context: Context, manga: Manga, fromCatalogue: Boolean = false): Intent {
SharedData.put(MangaEvent(manga)) SharedData.put(MangaEvent(manga))
@ -71,6 +74,7 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
fromCatalogue = intent.getBooleanExtra(FROM_CATALOGUE_EXTRA, false) fromCatalogue = intent.getBooleanExtra(FROM_CATALOGUE_EXTRA, false)
adapter = MangaDetailAdapter(supportFragmentManager, this) adapter = MangaDetailAdapter(supportFragmentManager, this)
view_pager.offscreenPageLimit = 3
view_pager.adapter = adapter view_pager.adapter = adapter
tabs.setupWithViewPager(view_pager) tabs.setupWithViewPager(view_pager)
@ -85,33 +89,50 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
setToolbarTitle(manga.title) setToolbarTitle(manga.title)
} }
internal class MangaDetailAdapter(fm: FragmentManager, activity: MangaActivity) : FragmentPagerAdapter(fm) { fun setTrackingIcon(visible: Boolean) {
val tab = tabs.getTabAt(TRACK_FRAGMENT) ?: return
val drawable = if (visible)
VectorDrawableCompat.create(resources, R.drawable.ic_done_white_18dp, null)
else null
private var pageCount: Int = 0 // I had no choice but to use reflection...
private val tabTitles = arrayOf(activity.getString(R.string.manga_detail_tab), val field = tab.javaClass.getDeclaredField("mView").apply { isAccessible = true }
activity.getString(R.string.manga_chapters_tab), "MAL") val view = field.get(tab) as LinearLayout
val textView = view.getChildAt(1) as TextView
textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null)
textView.compoundDrawablePadding = 4
}
private class MangaDetailAdapter(fm: FragmentManager, activity: MangaActivity)
: FragmentPagerAdapter(fm) {
private var tabCount = 2
private val tabTitles = listOf(
R.string.manga_detail_tab,
R.string.manga_chapters_tab,
R.string.manga_tracking_tab)
.map { activity.getString(it) }
init { init {
pageCount = 2 if (!activity.fromCatalogue && activity.presenter.trackManager.hasLoggedServices())
if (!activity.fromCatalogue && activity.presenter.syncManager.myAnimeList.isLogged) tabCount++
pageCount++
} }
override fun getCount(): Int { override fun getCount(): Int {
return pageCount return tabCount
} }
override fun getItem(position: Int): Fragment? { override fun getItem(position: Int): Fragment {
when (position) { when (position) {
INFO_FRAGMENT -> return MangaInfoFragment.newInstance() INFO_FRAGMENT -> return MangaInfoFragment.newInstance()
CHAPTERS_FRAGMENT -> return ChaptersFragment.newInstance() CHAPTERS_FRAGMENT -> return ChaptersFragment.newInstance()
MYANIMELIST_FRAGMENT -> return MyAnimeListFragment.newInstance() TRACK_FRAGMENT -> return TrackFragment.newInstance()
else -> return null else -> throw Exception("Unknown position")
} }
} }
override fun getPageTitle(position: Int): CharSequence { override fun getPageTitle(position: Int): CharSequence {
// Generate title based on item position
return tabTitles[position] return tabTitles[position]
} }

View File

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.manga
import android.os.Bundle import android.os.Bundle
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
import eu.kanade.tachiyomi.util.SharedData import eu.kanade.tachiyomi.util.SharedData
@ -22,9 +22,9 @@ class MangaPresenter : BasePresenter<MangaActivity>() {
val db: DatabaseHelper by injectLazy() val db: DatabaseHelper by injectLazy()
/** /**
* Manga sync manager. * Tracking manager.
*/ */
val syncManager: MangaSyncManager by injectLazy() val trackManager: TrackManager by injectLazy()
/** /**
* Manga associated with this instance. * Manga associated with this instance.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -163,19 +163,19 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
} }
override fun onBackPressed() { override fun onBackPressed() {
val chapterToUpdate = presenter.getMangaSyncChapterToUpdate() val chapterToUpdate = presenter.getTrackChapterToUpdate()
if (chapterToUpdate > 0) { if (chapterToUpdate > 0) {
if (preferences.askUpdateMangaSync()) { if (preferences.askUpdateTrack()) {
MaterialDialog.Builder(this) MaterialDialog.Builder(this)
.content(getString(R.string.confirm_update_manga_sync, chapterToUpdate)) .content(getString(R.string.confirm_update_manga_sync, chapterToUpdate))
.positiveText(android.R.string.yes) .positiveText(android.R.string.yes)
.negativeText(android.R.string.no) .negativeText(android.R.string.no)
.onPositive { dialog, which -> presenter.updateMangaSyncLastChapterRead() } .onPositive { dialog, which -> presenter.updateTrackLastChapterRead() }
.onAny { dialog1, which1 -> super.onBackPressed() } .onAny { dialog1, which1 -> super.onBackPressed() }
.show() .show()
} else { } else {
presenter.updateMangaSyncLastChapterRead() presenter.updateTrackLastChapterRead()
super.onBackPressed() super.onBackPressed()
} }
} else { } else {

View File

@ -10,14 +10,14 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaSync import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackUpdateService
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.reader.notification.ImageNotifier import eu.kanade.tachiyomi.ui.reader.notification.ImageNotifier
import eu.kanade.tachiyomi.util.DiskUtil import eu.kanade.tachiyomi.util.DiskUtil
@ -54,9 +54,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
val downloadManager: DownloadManager by injectLazy() val downloadManager: DownloadManager by injectLazy()
/** /**
* Sync manager. * Tracking manager.
*/ */
val syncManager: MangaSyncManager by injectLazy() val trackManager: TrackManager by injectLazy()
/** /**
* Source manager. * Source manager.
@ -124,7 +124,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
/** /**
* List of manga services linked to the active manga, or null if auto syncing is not enabled. * List of manga services linked to the active manga, or null if auto syncing is not enabled.
*/ */
private var mangaSyncList: List<MangaSync>? = null private var trackList: List<Track>? = null
/** /**
* Chapter loader whose job is to obtain the chapter list and initialize every page. * Chapter loader whose job is to obtain the chapter list and initialize every page.
@ -165,9 +165,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
.subscribeLatestCache({ view, manga -> view.onMangaOpen(manga) }) .subscribeLatestCache({ view, manga -> view.onMangaOpen(manga) })
// Retrieve the sync list if auto syncing is enabled. // Retrieve the sync list if auto syncing is enabled.
if (prefs.autoUpdateMangaSync()) { if (prefs.autoUpdateTrack()) {
add(db.getMangasSync(manga).asRxSingle() add(db.getTracks(manga).asRxSingle()
.subscribe({ mangaSyncList = it })) .subscribe({ trackList = it }))
} }
restartableLatestCache(LOAD_ACTIVE_CHAPTER, restartableLatestCache(LOAD_ACTIVE_CHAPTER,
@ -431,9 +431,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
/** /**
* Returns the chapter to be marked as last read in sync services or 0 if no update required. * Returns the chapter to be marked as last read in sync services or 0 if no update required.
*/ */
fun getMangaSyncChapterToUpdate(): Int { fun getTrackChapterToUpdate(): Int {
val mangaSyncList = mangaSyncList val trackList = trackList
if (chapter.pages == null || mangaSyncList == null || mangaSyncList.isEmpty()) if (chapter.pages == null || trackList == null || trackList.isEmpty())
return 0 return 0
val prevChapter = prevChapter val prevChapter = prevChapter
@ -446,24 +446,24 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
else else
0 0
mangaSyncList.forEach { sync -> trackList.forEach { sync ->
if (lastChapterRead > sync.last_chapter_read) { if (lastChapterRead > sync.last_chapter_read) {
sync.last_chapter_read = lastChapterRead sync.last_chapter_read = lastChapterRead
sync.update = true sync.update = true
} }
} }
return if (mangaSyncList.any { it.update }) lastChapterRead else 0 return if (trackList.any { it.update }) lastChapterRead else 0
} }
/** /**
* Starts the service that updates the last chapter read in sync services * Starts the service that updates the last chapter read in sync services
*/ */
fun updateMangaSyncLastChapterRead() { fun updateTrackLastChapterRead() {
mangaSyncList?.forEach { sync -> trackList?.forEach { sync ->
val service = syncManager.getService(sync.sync_id) val service = trackManager.getService(sync.sync_id)
if (service != null && service.isLogged && sync.update) { if (service != null && service.isLogged && sync.update) {
UpdateMangaSyncService.start(context, sync) TrackUpdateService.start(context, sync)
} }
} }
} }

View File

@ -7,14 +7,14 @@ import android.view.Gravity.CENTER
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ProgressBar import android.widget.ProgressBar
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager import eu.kanade.tachiyomi.data.track.TrackManager
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class AnilistLoginActivity : AppCompatActivity() { class AnilistLoginActivity : AppCompatActivity() {
private val syncManager: MangaSyncManager by injectLazy() private val trackManager: TrackManager by injectLazy()
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
@ -24,7 +24,7 @@ class AnilistLoginActivity : AppCompatActivity() {
val code = intent.data?.getQueryParameter("code") val code = intent.data?.getQueryParameter("code")
if (code != null) { if (code != null) {
syncManager.aniList.login(code) trackManager.aniList.login(code)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ .subscribe({
@ -33,7 +33,7 @@ class AnilistLoginActivity : AppCompatActivity() {
returnToSettings() returnToSettings()
}) })
} else { } else {
syncManager.aniList.logout() trackManager.aniList.logout()
returnToSettings() returnToSettings()
} }
} }

View File

@ -63,7 +63,7 @@ class SettingsActivity : BaseActivity(),
"general_screen" -> SettingsGeneralFragment.newInstance(key) "general_screen" -> SettingsGeneralFragment.newInstance(key)
"downloads_screen" -> SettingsDownloadsFragment.newInstance(key) "downloads_screen" -> SettingsDownloadsFragment.newInstance(key)
"sources_screen" -> SettingsSourcesFragment.newInstance(key) "sources_screen" -> SettingsSourcesFragment.newInstance(key)
"sync_screen" -> SettingsSyncFragment.newInstance(key) "tracking_screen" -> SettingsTrackingFragment.newInstance(key)
"advanced_screen" -> SettingsAdvancedFragment.newInstance(key) "advanced_screen" -> SettingsAdvancedFragment.newInstance(key)
"about_screen" -> SettingsAboutFragment.newInstance(key) "about_screen" -> SettingsAboutFragment.newInstance(key)
else -> SettingsFragment.newInstance(key) else -> SettingsFragment.newInstance(key)

View File

@ -28,7 +28,7 @@ open class SettingsFragment : XpPreferenceFragment() {
addPreferencesFromResource(R.xml.pref_reader) addPreferencesFromResource(R.xml.pref_reader)
addPreferencesFromResource(R.xml.pref_downloads) addPreferencesFromResource(R.xml.pref_downloads)
addPreferencesFromResource(R.xml.pref_sources) addPreferencesFromResource(R.xml.pref_sources)
addPreferencesFromResource(R.xml.pref_sync) addPreferencesFromResource(R.xml.pref_tracking)
addPreferencesFromResource(R.xml.pref_advanced) addPreferencesFromResource(R.xml.pref_advanced)
addPreferencesFromResource(R.xml.pref_about) addPreferencesFromResource(R.xml.pref_about)

View File

@ -1,89 +1,94 @@
package eu.kanade.tachiyomi.ui.setting package eu.kanade.tachiyomi.ui.setting
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.v7.preference.PreferenceCategory import android.support.customtabs.CustomTabsIntent
import android.support.v7.preference.XpPreferenceFragment import android.support.v7.preference.PreferenceCategory
import android.view.View import android.support.v7.preference.XpPreferenceFragment
import eu.kanade.tachiyomi.R import android.view.View
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.widget.preference.LoginPreference import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.widget.preference.MangaSyncLoginDialog import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
import uy.kohesive.injekt.injectLazy import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.widget.preference.LoginPreference
class SettingsSyncFragment : SettingsFragment() { import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog
import uy.kohesive.injekt.injectLazy
companion object {
const val SYNC_CHANGE_REQUEST = 121 class SettingsTrackingFragment : SettingsFragment() {
fun newInstance(rootKey: String): SettingsSyncFragment { companion object {
val args = Bundle() const val SYNC_CHANGE_REQUEST = 121
args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey)
return SettingsSyncFragment().apply { arguments = args } fun newInstance(rootKey: String): SettingsTrackingFragment {
} val args = Bundle()
} args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey)
return SettingsTrackingFragment().apply { arguments = args }
private val syncManager: MangaSyncManager by injectLazy() }
}
private val preferences: PreferencesHelper by injectLazy()
private val trackManager: TrackManager by injectLazy()
val syncCategory: PreferenceCategory by bindPref(R.string.pref_category_manga_sync_accounts_key)
private val preferences: PreferencesHelper by injectLazy()
override fun onViewCreated(view: View, savedState: Bundle?) {
super.onViewCreated(view, savedState) val syncCategory: PreferenceCategory by bindPref(R.string.pref_category_tracking_accounts_key)
registerService(syncManager.myAnimeList) override fun onViewCreated(view: View, savedState: Bundle?) {
super.onViewCreated(view, savedState)
// registerService(syncManager.aniList) {
// val intent = CustomTabsIntent.Builder() registerService(trackManager.myAnimeList)
// .setToolbarColor(activity.theme.getResourceColor(R.attr.colorPrimary))
// .build() registerService(trackManager.aniList) {
// intent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) val intent = CustomTabsIntent.Builder()
// intent.launchUrl(activity, AnilistApi.authUrl()) .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) { registerService(trackManager.kitsu)
}
LoginPreference(preferenceManager.context).apply {
key = preferences.keys.syncUsername(service.id) private fun <T : TrackService> registerService(
title = service.name service: T,
onPreferenceClick: (T) -> Unit = defaultOnPreferenceClick) {
setOnPreferenceClickListener {
onPreferenceClick(service) LoginPreference(preferenceManager.context).apply {
true key = preferences.keys.trackUsername(service.id)
} title = service.name
syncCategory.addPreference(this) setOnPreferenceClickListener {
} onPreferenceClick(service)
} true
}
private val defaultOnPreferenceClick: (MangaSyncService) -> Unit
get() = { syncCategory.addPreference(this)
val fragment = MangaSyncLoginDialog.newInstance(it) }
fragment.setTargetFragment(this, SYNC_CHANGE_REQUEST) }
fragment.show(fragmentManager, null)
} private val defaultOnPreferenceClick: (TrackService) -> Unit
get() = {
override fun onResume() { val fragment = TrackLoginDialog.newInstance(it)
super.onResume() fragment.setTargetFragment(this, SYNC_CHANGE_REQUEST)
// Manually refresh anilist holder fragment.show(fragmentManager, null)
// updatePreference(syncManager.aniList.id) }
}
override fun onResume() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onResume()
if (requestCode == SYNC_CHANGE_REQUEST) { // Manually refresh anilist holder
updatePreference(resultCode) updatePreference(trackManager.aniList.id)
} }
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
private fun updatePreference(id: Int) { if (requestCode == SYNC_CHANGE_REQUEST) {
val pref = findPreference(preferences.keys.syncUsername(id)) as? LoginPreference updatePreference(resultCode)
pref?.notifyChanged() }
} }
} private fun updatePreference(id: Int) {
val pref = findPreference(preferences.keys.trackUsername(id)) as? LoginPreference
pref?.notifyChanged()
}
}

View File

@ -3,20 +3,20 @@ package eu.kanade.tachiyomi.widget.preference
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.pref_account_login.view.* import kotlinx.android.synthetic.main.pref_account_login.view.*
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class MangaSyncLoginDialog : LoginDialogPreference() { class TrackLoginDialog : LoginDialogPreference() {
companion object { companion object {
fun newInstance(sync: MangaSyncService): LoginDialogPreference { fun newInstance(sync: TrackService): LoginDialogPreference {
val fragment = MangaSyncLoginDialog() val fragment = TrackLoginDialog()
val bundle = Bundle(1) val bundle = Bundle(1)
bundle.putInt("key", sync.id) bundle.putInt("key", sync.id)
fragment.arguments = bundle fragment.arguments = bundle
@ -24,15 +24,15 @@ class MangaSyncLoginDialog : LoginDialogPreference() {
} }
} }
val syncManager: MangaSyncManager by injectLazy() val trackManager: TrackManager by injectLazy()
lateinit var sync: MangaSyncService lateinit var sync: TrackService
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val syncId = arguments.getInt("key") val syncId = arguments.getInt("key")
sync = syncManager.getService(syncId)!! sync = trackManager.getService(syncId)!!
} }
override fun setCredentialsOnView(view: View) = with(view) { override fun setCredentialsOnView(view: View) = with(view) {
@ -56,11 +56,9 @@ class MangaSyncLoginDialog : LoginDialogPreference() {
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ .subscribe({
sync.saveCredentials(user, pass)
dialog.dismiss() dialog.dismiss()
context.toast(R.string.login_success) context.toast(R.string.login_success)
}, { error -> }, { error ->
sync.logout()
login.progress = -1 login.progress = -1
login.setText(R.string.unknown_error) login.setText(R.string.unknown_error)
}) })

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -14,11 +14,11 @@
android:paddingRight="@dimen/margin_right"> android:paddingRight="@dimen/margin_right">
<EditText <EditText
android:id="@+id/myanimelist_search_field" android:id="@+id/track_search"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:hint="@string/title_hint"/> android:hint="@string/title"/>
</LinearLayout> </LinearLayout>
@ -33,7 +33,7 @@
android:visibility="gone"/> android:visibility="gone"/>
<ListView <ListView
android:id="@+id/myanimelist_search_results" android:id="@+id/track_search_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:choiceMode="singleChoice" android:choiceMode="singleChoice"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -65,7 +65,7 @@
<string name="pref_category_reader">Lector</string> <string name="pref_category_reader">Lector</string>
<string name="pref_category_downloads">Descargas</string> <string name="pref_category_downloads">Descargas</string>
<string name="pref_category_sources">Fuentes</string> <string name="pref_category_sources">Fuentes</string>
<string name="pref_category_sync">Sincronización</string> <string name="pref_category_tracking">Seguimiento</string>
<string name="pref_category_advanced">Avanzado</string> <string name="pref_category_advanced">Avanzado</string>
<string name="pref_category_about">Acerca de</string> <string name="pref_category_about">Acerca de</string>
@ -232,7 +232,7 @@
<string name="on_hold">En espera</string> <string name="on_hold">En espera</string>
<string name="plan_to_read">Para leer luego</string> <string name="plan_to_read">Para leer luego</string>
<string name="score">Puntuación</string> <string name="score">Puntuación</string>
<string name="title_hint">Título</string> <string name="title">Título</string>
<string name="status">Estado</string> <string name="status">Estado</string>
<string name="chapters">Capítulos</string> <string name="chapters">Capítulos</string>

View File

@ -240,7 +240,7 @@
<string name="on_hold">Em espera</string> <string name="on_hold">Em espera</string>
<string name="plan_to_read">Planeada a leitura</string> <string name="plan_to_read">Planeada a leitura</string>
<string name="score">Avaliação</string> <string name="score">Avaliação</string>
<string name="title_hint">Título</string> <string name="title">Título</string>
<string name="status">Estado</string> <string name="status">Estado</string>
<string name="chapters">Capítulos</string> <string name="chapters">Capítulos</string>

View File

@ -3,7 +3,7 @@
<string name="pref_category_general_key">pref_category_general_key</string> <string name="pref_category_general_key">pref_category_general_key</string>
<string name="pref_category_reader_key">pref_category_reader_key</string> <string name="pref_category_reader_key">pref_category_reader_key</string>
<string name="pref_category_sync_key">pref_category_sync_key</string> <string name="pref_category_tracking_key">pref_category_tracking_key</string>
<string name="pref_category_downloads_key">pref_category_downloads_key</string> <string name="pref_category_downloads_key">pref_category_downloads_key</string>
<string name="pref_category_advanced_key">pref_category_advanced_key</string> <string name="pref_category_advanced_key">pref_category_advanced_key</string>
<string name="pref_category_about_key">pref_category_about_key</string> <string name="pref_category_about_key">pref_category_about_key</string>
@ -52,7 +52,7 @@
<string name="pref_last_used_category_key">last_used_category</string> <string name="pref_last_used_category_key">last_used_category</string>
<string name="pref_source_languages">pref_source_languages</string> <string name="pref_source_languages">pref_source_languages</string>
<string name="pref_category_manga_sync_accounts_key">category_manga_sync_accounts</string> <string name="pref_category_tracking_accounts_key">category_tracking_accounts</string>
<string name="pref_clear_chapter_cache_key">pref_clear_chapter_cache_key</string> <string name="pref_clear_chapter_cache_key">pref_clear_chapter_cache_key</string>
<string name="pref_clear_database_key">pref_clear_database_key</string> <string name="pref_clear_database_key">pref_clear_database_key</string>

View File

@ -80,7 +80,7 @@
<string name="pref_category_reader">Reader</string> <string name="pref_category_reader">Reader</string>
<string name="pref_category_downloads">Downloads</string> <string name="pref_category_downloads">Downloads</string>
<string name="pref_category_sources">Sources</string> <string name="pref_category_sources">Sources</string>
<string name="pref_category_sync">Sync</string> <string name="pref_category_tracking">Tracking</string>
<string name="pref_category_advanced">Advanced</string> <string name="pref_category_advanced">Advanced</string>
<string name="pref_category_about">About</string> <string name="pref_category_about">About</string>
@ -276,13 +276,14 @@
<string name="confirm_delete_chapters">Are you sure you want to delete selected chapters?</string> <string name="confirm_delete_chapters">Are you sure you want to delete selected chapters?</string>
<!-- MyAnimeList fragment --> <!-- MyAnimeList fragment -->
<string name="manga_tracking_tab">Tracking</string>
<string name="reading">Reading</string> <string name="reading">Reading</string>
<string name="completed">Completed</string> <string name="completed">Completed</string>
<string name="dropped">Dropped</string> <string name="dropped">Dropped</string>
<string name="on_hold">On hold</string> <string name="on_hold">On hold</string>
<string name="plan_to_read">Plan to read</string> <string name="plan_to_read">Plan to read</string>
<string name="score">Score</string> <string name="score">Score</string>
<string name="title_hint">Title</string> <string name="title">Title</string>
<string name="status">Status</string> <string name="status">Status</string>
<string name="chapters">Chapters</string> <string name="chapters">Chapters</string>

View File

@ -1,30 +1,32 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceScreen <PreferenceScreen
android:icon="@drawable/ic_sync_black_24dp" android:icon="@drawable/ic_sync_black_24dp"
android:key="sync_screen" android:key="tracking_screen"
android:persistent="false" android:persistent="false"
android:title="@string/pref_category_sync" android:title="@string/pref_category_tracking"
app:asp_tintEnabled="true"> app:asp_tintEnabled="true">
<SwitchPreference <SwitchPreference
android:key="@string/pref_auto_update_manga_sync_key" android:key="@string/pref_auto_update_manga_sync_key"
android:title="@string/pref_auto_update_manga_sync" android:title="@string/pref_auto_update_manga_sync"
android:defaultValue="true" /> android:defaultValue="true"
app:showText="false"/>
<SwitchPreference <SwitchPreference
android:key="@string/pref_ask_update_manga_sync_key" android:key="@string/pref_ask_update_manga_sync_key"
android:title="@string/pref_ask_update_manga_sync" android:title="@string/pref_ask_update_manga_sync"
android:defaultValue="false" 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 <PreferenceCategory
android:key="@string/pref_category_manga_sync_accounts_key" android:key="@string/pref_category_tracking_accounts_key"
android:title="@string/services" android:title="@string/services"
android:persistent="false" /> android:persistent="false"
app:showText="false"/>
</PreferenceScreen> </PreferenceScreen>

View File

@ -390,16 +390,16 @@ class BackupTest {
@Test @Test
fun testRestoreSyncForManga() { fun testRestoreSyncForManga() {
// Create a manga and mangaSync // Create a manga and track
val manga = createManga("title") val manga = createManga("title")
manga.id = 1L manga.id = 1L
val mangaSync = createMangaSync(manga, 1, 2, 3) val track = createTrack(manga, 1, 2, 3)
// Add an entry for the manga // Add an entry for the manga
val entry = JsonObject() val entry = JsonObject()
entry.add("manga", toJson(manga)) entry.add("manga", toJson(manga))
entry.add("sync", toJson(mangaSync)) entry.add("sync", toJson(track))
// Append the entry to the backup list // Append the entry to the backup list
val mangas = ArrayList<JsonElement>() val mangas = ArrayList<JsonElement>()
@ -412,7 +412,7 @@ class BackupTest {
val dbManga = db.getManga(1).executeAsBlocking() val dbManga = db.getManga(1).executeAsBlocking()
assertThat(dbManga).isNotNull() assertThat(dbManga).isNotNull()
val dbSync = db.getMangasSync(dbManga!!).executeAsBlocking() val dbSync = db.getTracks(dbManga!!).executeAsBlocking()
assertThat(dbSync).hasSize(3) assertThat(dbSync).hasSize(3)
} }
@ -422,13 +422,13 @@ class BackupTest {
// Create a manga and 3 sync // Create a manga and 3 sync
val manga = createManga("title") val manga = createManga("title")
manga.id = mangaId manga.id = mangaId
val mangaSync = createMangaSync(manga, 1, 2, 3) val track = createTrack(manga, 1, 2, 3)
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
// Add an entry for the manga // Add an entry for the manga
val entry = JsonObject() val entry = JsonObject()
entry.add("manga", toJson(manga)) entry.add("manga", toJson(manga))
entry.add("sync", toJson(mangaSync)) entry.add("sync", toJson(track))
// Append the entry to the backup list // Append the entry to the backup list
val mangas = ArrayList<JsonElement>() val mangas = ArrayList<JsonElement>()
@ -441,7 +441,7 @@ class BackupTest {
val dbManga = db.getManga(mangaId).executeAsBlocking() val dbManga = db.getManga(mangaId).executeAsBlocking()
assertThat(dbManga).isNotNull() assertThat(dbManga).isNotNull()
val dbSync = db.getMangasSync(dbManga!!).executeAsBlocking() val dbSync = db.getTracks(dbManga!!).executeAsBlocking()
assertThat(dbSync).hasSize(3) assertThat(dbSync).hasSize(3)
} }
@ -451,17 +451,17 @@ class BackupTest {
// Store a manga and 3 sync // Store a manga and 3 sync
val manga = createManga("title") val manga = createManga("title")
manga.id = mangaId manga.id = mangaId
var mangaSync = createMangaSync(manga, 1, 2, 3) var track = createTrack(manga, 1, 2, 3)
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
db.insertMangasSync(mangaSync).executeAsBlocking() db.insertTracks(track).executeAsBlocking()
// The backup contains a existing sync and a new one, so it should have 4 sync // The backup contains a existing sync and a new one, so it should have 4 sync
mangaSync = createMangaSync(manga, 3, 4) track = createTrack(manga, 3, 4)
// Add an entry for the manga // Add an entry for the manga
val entry = JsonObject() val entry = JsonObject()
entry.add("manga", toJson(manga)) entry.add("manga", toJson(manga))
entry.add("sync", toJson(mangaSync)) entry.add("sync", toJson(track))
// Append the entry to the backup list // Append the entry to the backup list
val mangas = ArrayList<JsonElement>() val mangas = ArrayList<JsonElement>()
@ -474,7 +474,7 @@ class BackupTest {
val dbManga = db.getManga(mangaId).executeAsBlocking() val dbManga = db.getManga(mangaId).executeAsBlocking()
assertThat(dbManga).isNotNull() assertThat(dbManga).isNotNull()
val dbSync = db.getMangasSync(dbManga!!).executeAsBlocking() val dbSync = db.getTracks(dbManga!!).executeAsBlocking()
assertThat(dbSync).hasSize(4) assertThat(dbSync).hasSize(4)
} }
@ -546,17 +546,17 @@ class BackupTest {
return chapters return chapters
} }
private fun createMangaSync(manga: Manga, syncId: Int): MangaSync { private fun createTrack(manga: Manga, syncId: Int): Track {
val m = MangaSync.create(syncId) val m = Track.create(syncId)
m.manga_id = manga.id!! m.manga_id = manga.id!!
m.title = "title" m.title = "title"
return m return m
} }
private fun createMangaSync(manga: Manga, vararg syncIds: Int): List<MangaSync> { private fun createTrack(manga: Manga, vararg syncIds: Int): List<Track> {
val ms = ArrayList<MangaSync>() val ms = ArrayList<Track>()
for (title in syncIds) { for (title in syncIds) {
ms.add(createMangaSync(manga, title)) ms.add(createTrack(manga, title))
} }
return ms return ms
} }