diff --git a/.travis.yml b/.travis.yml index aa62653c5..e60d26ac2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,11 +12,21 @@ android: - extra-android-support - extra-google-google_play_services + licenses: + - android-sdk-license-.+ + - '.+' + jdk: - oraclejdk8 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 script: "./gradlew clean buildStandardDebug" sudo: false diff --git a/app/build.gradle b/app/build.gradle index 935e5e5e9..2bc0e4b59 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -110,6 +110,8 @@ dependencies { compile "com.android.support:support-annotations:$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' // ReactiveX diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3481942e4..2d21a2262 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -53,6 +53,18 @@ android:label="@string/app_name" android:theme="@style/FilePickerTheme"> + + + + + + + + - >(element.get(CHAPTERS) ?: JsonArray()) - val sync = gson.fromJson>(element.get(MANGA_SYNC) ?: JsonArray()) + val tracks = gson.fromJson>(element.get(TRACK) ?: JsonArray()) val categories = gson.fromJson>(element.get(CATEGORIES) ?: JsonArray()) // Restore everything related to this manga restoreManga(manga) restoreChaptersForManga(manga, chapters) - restoreSyncForManga(manga, sync) + restoreSyncForManga(manga, tracks) restoreCategoriesForManga(manga, categories) } } @@ -333,35 +333,35 @@ class BackupManager(private val db: DatabaseHelper) { * Restores the sync of a manga. * * @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) { + private fun restoreSyncForManga(manga: Manga, tracks: List) { // Fix foreign keys with the current manga id - for (mangaSync in sync) { - mangaSync.manga_id = manga.id!! + for (track in tracks) { + track.manga_id = manga.id!! } - val dbSyncs = db.getMangasSync(manga).executeAsBlocking() - val syncToUpdate = ArrayList() - for (backupSync in sync) { + val dbTracks = db.getTracks(manga).executeAsBlocking() + val trackToUpdate = ArrayList() + for (backupTrack in tracks) { // Try to find existing chapter in db - val pos = dbSyncs.indexOf(backupSync) + val pos = dbTracks.indexOf(backupTrack) if (pos != -1) { // 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 - dbSync.last_chapter_read = Math.max(backupSync.last_chapter_read, dbSync.last_chapter_read) - syncToUpdate.add(dbSync) + dbSync.last_chapter_read = Math.max(backupTrack.last_chapter_read, dbSync.last_chapter_read) + trackToUpdate.add(dbSync) } else { // Insert new sync. Let the db assign the id - backupSync.id = null - syncToUpdate.add(backupSync) + backupTrack.id = null + trackToUpdate.add(backupTrack) } } // Update database - if (!syncToUpdate.isEmpty()) { - db.insertMangasSync(syncToUpdate).executeAsBlocking() + if (!trackToUpdate.isEmpty()) { + db.insertTracks(trackToUpdate).executeAsBlocking() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/IdExclusion.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/IdExclusion.kt index 92c95cb03..d1d3b7e20 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/IdExclusion.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/IdExclusion.kt @@ -5,7 +5,7 @@ import com.google.gson.FieldAttributes import eu.kanade.tachiyomi.data.database.models.CategoryImpl import eu.kanade.tachiyomi.data.database.models.ChapterImpl 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 { @@ -17,7 +17,7 @@ class IdExclusion : ExclusionStrategy { override fun shouldSkipField(f: FieldAttributes) = when (f.declaringClass) { MangaImpl::class.java -> mangaExclusions.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) else -> false } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt index 343479cc0..76bbda1d5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt @@ -10,13 +10,13 @@ import eu.kanade.tachiyomi.data.database.queries.* * This class provides operations to manage the database through its interfaces. */ open class DatabaseHelper(context: Context) -: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries { +: MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries { override val db = DefaultStorIOSQLite.builder() .sqliteOpenHelper(DbOpenHelper(context)) .addTypeMapping(Manga::class.java, MangaTypeMapping()) .addTypeMapping(Chapter::class.java, ChapterTypeMapping()) - .addTypeMapping(MangaSync::class.java, MangaSyncTypeMapping()) + .addTypeMapping(Track::class.java, TrackTypeMapping()) .addTypeMapping(Category::class.java, CategoryTypeMapping()) .addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping()) .addTypeMapping(History::class.java, HistoryTypeMapping()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenHelper.kt index 4687a0e38..40e5d129a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenHelper.kt @@ -23,7 +23,7 @@ class DbOpenHelper(context: Context) override fun onCreate(db: SQLiteDatabase) = with(db) { execSQL(MangaTable.createTableQuery) execSQL(ChapterTable.createTableQuery) - execSQL(MangaSyncTable.createTableQuery) + execSQL(TrackTable.createTableQuery) execSQL(CategoryTable.createTableQuery) execSQL(MangaCategoryTable.createTableQuery) execSQL(HistoryTable.createTableQuery) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaSyncTypeMapping.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt similarity index 53% rename from app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaSyncTypeMapping.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt index b02ae16a5..3d66b104c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaSyncTypeMapping.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/TrackTypeMapping.kt @@ -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.InsertQuery import com.pushtorefresh.storio.sqlite.queries.UpdateQuery -import eu.kanade.tachiyomi.data.database.models.MangaSync -import eu.kanade.tachiyomi.data.database.models.MangaSyncImpl -import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_ID -import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_LAST_CHAPTER_READ -import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_MANGA_ID -import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_REMOTE_ID -import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_SCORE -import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_STATUS -import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_SYNC_ID -import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_TITLE -import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_TOTAL_CHAPTERS -import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.TABLE +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.database.models.TrackImpl +import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID +import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ +import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID +import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_REMOTE_ID +import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE +import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS +import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID +import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE +import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TOTAL_CHAPTERS +import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE -class MangaSyncTypeMapping : SQLiteTypeMapping( - MangaSyncPutResolver(), - MangaSyncGetResolver(), - MangaSyncDeleteResolver() +class TrackTypeMapping : SQLiteTypeMapping( + TrackPutResolver(), + TrackGetResolver(), + TrackDeleteResolver() ) -class MangaSyncPutResolver : DefaultPutResolver() { +class TrackPutResolver : DefaultPutResolver() { - override fun mapToInsertQuery(obj: MangaSync) = InsertQuery.builder() + override fun mapToInsertQuery(obj: Track) = InsertQuery.builder() .table(TABLE) .build() - override fun mapToUpdateQuery(obj: MangaSync) = UpdateQuery.builder() + override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder() .table(TABLE) .where("$COL_ID = ?") .whereArgs(obj.id) .build() - override fun mapToContentValues(obj: MangaSync) = ContentValues(9).apply { + override fun mapToContentValues(obj: Track) = ContentValues(9).apply { put(COL_ID, obj.id) put(COL_MANGA_ID, obj.manga_id) put(COL_SYNC_ID, obj.sync_id) @@ -53,9 +53,9 @@ class MangaSyncPutResolver : DefaultPutResolver() { } } -class MangaSyncGetResolver : DefaultGetResolver() { +class TrackGetResolver : DefaultGetResolver() { - override fun mapFromCursor(cursor: Cursor): MangaSync = MangaSyncImpl().apply { + override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply { id = cursor.getLong(cursor.getColumnIndex(COL_ID)) manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)) sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID)) @@ -68,9 +68,9 @@ class MangaSyncGetResolver : DefaultGetResolver() { } } -class MangaSyncDeleteResolver : DefaultDeleteResolver() { +class TrackDeleteResolver : DefaultDeleteResolver() { - override fun mapToDeleteQuery(obj: MangaSync) = DeleteQuery.builder() + override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder() .table(TABLE) .where("$COL_ID = ?") .whereArgs(obj.id) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaSync.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt similarity index 76% rename from app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaSync.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt index c1e4664fd..ce04b5303 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt @@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.database.models import java.io.Serializable -interface MangaSync : Serializable { +interface Track : Serializable { var id: Long? @@ -24,7 +24,7 @@ interface MangaSync : Serializable { var update: Boolean - fun copyPersonalFrom(other: MangaSync) { + fun copyPersonalFrom(other: Track) { last_chapter_read = other.last_chapter_read score = other.score status = other.status @@ -32,7 +32,7 @@ interface MangaSync : Serializable { companion object { - fun create(serviceId: Int): MangaSync = MangaSyncImpl().apply { + fun create(serviceId: Int): Track = TrackImpl().apply { sync_id = serviceId } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaSyncImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt similarity index 77% rename from app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaSyncImpl.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt index 95d3bc4a7..f94c85993 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaSyncImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt @@ -1,6 +1,6 @@ package eu.kanade.tachiyomi.data.database.models -class MangaSyncImpl : MangaSync { +class TrackImpl : Track { override var id: Long? = null @@ -26,11 +26,11 @@ class MangaSyncImpl : MangaSync { if (this === other) return true 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 (sync_id != mangaSync.sync_id) return false - return remote_id == mangaSync.remote_id + if (manga_id != other.manga_id) return false + if (sync_id != other.sync_id) return false + return remote_id == other.remote_id } override fun hashCode(): Int { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaSyncQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaSyncQueries.kt deleted file mode 100644 index 5140b516e..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaSyncQueries.kt +++ /dev/null @@ -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) = 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() - -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/TrackQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/TrackQueries.kt new file mode 100644 index 000000000..e215e72ea --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/TrackQueries.kt @@ -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) = 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() + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaSyncTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt similarity index 97% rename from app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaSyncTable.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt index 4a42d5fb9..94622cc33 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaSyncTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt @@ -1,6 +1,6 @@ package eu.kanade.tachiyomi.data.database.tables -object MangaSyncTable { +object TrackTable { const val TABLE = "manga_sync" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncManager.kt deleted file mode 100644 index 9a0093783..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncManager.kt +++ /dev/null @@ -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 } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncService.kt deleted file mode 100644 index ae653b0f0..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncService.kt +++ /dev/null @@ -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 - - abstract fun update(manga: MangaSync): Observable - - abstract fun bind(manga: MangaSync): Observable - - 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) - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/Anilist.kt b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/Anilist.kt deleted file mode 100644 index dff6f530f..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/Anilist.kt +++ /dev/null @@ -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> { - return api.search(query, 1) - .flatMap { Observable.from(it) } - .filter { it.type != "Novel" } - .map { it.toMangaSync() } - .toList() - } - - fun getList(): Observable> { - return api.getList(getUsername()) - .flatMap { Observable.from(it.flatten()) } - .map { it.toMangaSync() } - .toList() - } - - override fun add(manga: MangaSync): Observable { - 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 { - 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 { - 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") - } - -} - diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index f51f1dcb9..9f0681371 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -51,9 +51,9 @@ class PreferenceKeys(context: Context) { 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) @@ -95,9 +95,11 @@ class PreferenceKeys(context: Context) { 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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 65873180f..66740a4fa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -7,8 +7,8 @@ import android.preference.PreferenceManager import com.f2prateek.rx.preferences.Preference import com.f2prateek.rx.preferences.RxSharedPreferences import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.mangasync.MangaSyncService import eu.kanade.tachiyomi.data.source.Source +import eu.kanade.tachiyomi.data.track.TrackService import java.io.File fun Preference.getOrDefault(): T = get() ?: defaultValue()!! @@ -70,9 +70,9 @@ class PreferencesHelper(val context: Context) { 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) @@ -95,17 +95,21 @@ class PreferencesHelper(val context: Context) { .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() - .putString(keys.syncUsername(sync.id), username) - .putString(keys.syncPassword(sync.id), password) + .putString(keys.trackUsername(sync.id), username) + .putString(keys.trackPassword(sync.id), password) .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 downloadThreads() = rxPrefs.getInteger(keys.downloadThreads, 1) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.kt index 862915e6e..157685871 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.kt @@ -1,6 +1,5 @@ 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.Manga import eu.kanade.tachiyomi.data.source.EN diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt new file mode 100644 index 000000000..46f3c99e9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt @@ -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 } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt new file mode 100644 index 000000000..4d52ee698 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt @@ -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 + + abstract fun update(track: Track): Observable + + abstract fun bind(track: Track): Observable + + abstract fun search(query: String): Observable> + + abstract fun refresh(track: Track): Observable + + abstract fun getStatus(status: Int): String + + abstract fun getStatusList(): List + + @DrawableRes + abstract fun getLogo(): Int + + abstract fun getLogoColor(): Int + + // TODO better support (decimals) + abstract fun maxScore(): Int + + abstract fun formatScore(track: Track): String + + fun saveCredentials(username: String, password: String) { + preferences.setTrackCredentials(this, username, password) + } + + @CallSuper + open fun logout() { + preferences.setTrackCredentials(this, "", "") + } + + fun getUsername() = preferences.trackUsername(this) + + fun getPassword() = preferences.trackPassword(this) + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/UpdateMangaSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackUpdateService.kt similarity index 61% rename from app/src/main/java/eu/kanade/tachiyomi/data/mangasync/UpdateMangaSyncService.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/TrackUpdateService.kt index 84181b555..06609cfc6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/UpdateMangaSyncService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackUpdateService.kt @@ -1,20 +1,20 @@ -package eu.kanade.tachiyomi.data.mangasync +package eu.kanade.tachiyomi.data.track import android.app.Service import android.content.Context import android.content.Intent import android.os.IBinder 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.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import rx.subscriptions.CompositeSubscription 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() private lateinit var subscriptions: CompositeSubscription @@ -30,9 +30,9 @@ class UpdateMangaSyncService : Service() { } override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { - val manga = intent.getSerializableExtra(EXTRA_MANGASYNC) - if (manga != null) { - updateLastChapterRead(manga as MangaSync, startId) + val track = intent.getSerializableExtra(EXTRA_TRACK) + if (track != null) { + updateLastChapterRead(track as Track, startId) return Service.START_REDELIVER_INTENT } else { stopSelf(startId) @@ -44,15 +44,15 @@ class UpdateMangaSyncService : Service() { return null } - private fun updateLastChapterRead(mangaSync: MangaSync, startId: Int) { - val sync = syncManager.getService(mangaSync.sync_id) + private fun updateLastChapterRead(track: Track, startId: Int) { + val sync = trackManager.getService(track.sync_id) if (sync == null) { stopSelf(startId) return } - subscriptions.add(Observable.defer { sync.update(mangaSync) } - .flatMap { db.insertMangaSync(mangaSync).asRxObservable() } + subscriptions.add(Observable.defer { sync.update(track) } + .flatMap { db.insertTrack(track).asRxObservable() } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ stopSelf(startId) }, @@ -61,12 +61,12 @@ class UpdateMangaSyncService : Service() { companion object { - private val EXTRA_MANGASYNC = "extra_mangasync" + private val EXTRA_TRACK = "extra_track" @JvmStatic - fun start(context: Context, mangaSync: MangaSync) { - val intent = Intent(context, UpdateMangaSyncService::class.java) - intent.putExtra(EXTRA_MANGASYNC, mangaSync) + fun start(context: Context, track: Track) { + val intent = Intent(context, TrackUpdateService::class.java) + intent.putExtra(EXTRA_TRACK, track) context.startService(intent) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt new file mode 100644 index 000000000..56464aec4 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt @@ -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> { + return api.search(query, 1) + .flatMap { Observable.from(it) } + .filter { it.type != "Novel" } + .map { it.toTrack() } + .toList() + } + + fun getList(): Observable> { + return api.getList(getUsername()) + .flatMap { Observable.from(it.flatten()) } + .map { it.toTrack() } + .toList() + } + + override fun add(track: Track): Observable { + return api.addManga(track.remote_id, track.last_chapter_read, track.getAnilistStatus()) + .doOnNext { it.body().close() } + .doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") } + .doOnError { Timber.e(it) } + .map { track } + } + + override fun update(track: Track): Observable { + if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { + track.status = COMPLETED + } + return api.updateManga(track.remote_id, track.last_chapter_read, track.getAnilistStatus(), + track.getAnilistScore()) + .doOnNext { it.body().close() } + .doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") } + .doOnError { Timber.e(it) } + .map { track } + } + + override fun bind(track: Track): Observable { + return getList() + .flatMap { userlist -> + track.sync_id = id + val remoteTrack = userlist.find { it.remote_id == track.remote_id } + if (remoteTrack != null) { + track.copyPersonalFrom(remoteTrack) + update(track) + } else { + // Set default fields if it's not found in the list + track.score = DEFAULT_SCORE.toFloat() + track.status = DEFAULT_STATUS + add(track) + } + } + } + + override fun refresh(track: Track): Observable { + return getList() + .map { myList -> + val remoteTrack = myList.find { it.remote_id == track.remote_id } + if (remoteTrack != null) { + track.copyPersonalFrom(remoteTrack) + track.total_chapters = remoteTrack.total_chapters + track + } else { + throw Exception("Could not find manga") + } + } + } + + override fun getStatusList(): List { + return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) + } + + 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() + } + +} + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt similarity index 87% rename from app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/AnilistApi.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index 6280562be..8a4868432 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -1,89 +1,88 @@ -package eu.kanade.tachiyomi.data.mangasync.anilist - -import android.net.Uri -import com.google.gson.JsonObject -import eu.kanade.tachiyomi.data.mangasync.anilist.model.ALManga -import eu.kanade.tachiyomi.data.mangasync.anilist.model.ALUserLists -import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth -import eu.kanade.tachiyomi.data.network.POST -import okhttp3.FormBody -import okhttp3.OkHttpClient -import okhttp3.ResponseBody -import retrofit2.Response -import retrofit2.Retrofit -import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory -import retrofit2.converter.gson.GsonConverterFactory -import retrofit2.http.* -import rx.Observable - -interface AnilistApi { - - companion object { - private const val clientId = "tachiyomi-hrtje" - private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C" - private const val clientUrl = "tachiyomi://anilist-auth" - private const val baseUrl = "https://anilist.co/api/" - - fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon() - .appendQueryParameter("grant_type", "authorization_code") - .appendQueryParameter("client_id", clientId) - .appendQueryParameter("redirect_uri", clientUrl) - .appendQueryParameter("response_type", "code") - .build() - - fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token", - body = FormBody.Builder() - .add("grant_type", "refresh_token") - .add("client_id", clientId) - .add("client_secret", clientSecret) - .add("refresh_token", token) - .build()) - - fun createService(client: OkHttpClient) = Retrofit.Builder() - .baseUrl(baseUrl) - .client(client) - .addConverterFactory(GsonConverterFactory.create()) - .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) - .build() - .create(AnilistApi::class.java) - - } - - @FormUrlEncoded - @POST("auth/access_token") - fun requestAccessToken( - @Field("code") code: String, - @Field("grant_type") grant_type: String = "authorization_code", - @Field("client_id") client_id: String = clientId, - @Field("client_secret") client_secret: String = clientSecret, - @Field("redirect_uri") redirect_uri: String = clientUrl) - : Observable - - @GET("user") - fun getCurrentUser(): Observable - - @GET("manga/search/{query}") - fun search(@Path("query") query: String, @Query("page") page: Int): Observable> - - @GET("user/{username}/mangalist") - fun getList(@Path("username") username: String): Observable - - @FormUrlEncoded - @PUT("mangalist") - fun addManga( - @Field("id") id: Int, - @Field("chapters_read") chapters_read: Int, - @Field("list_status") list_status: String, - @Field("score_raw") score_raw: Int) - : Observable> - - @FormUrlEncoded - @PUT("mangalist") - fun updateManga( - @Field("id") id: Int, - @Field("chapters_read") chapters_read: Int, - @Field("list_status") list_status: String, - @Field("score_raw") score_raw: Int) - : Observable> - +package eu.kanade.tachiyomi.data.track.anilist + +import android.net.Uri +import com.google.gson.JsonObject +import eu.kanade.tachiyomi.data.network.POST +import eu.kanade.tachiyomi.data.track.anilist.model.ALManga +import eu.kanade.tachiyomi.data.track.anilist.model.ALUserLists +import eu.kanade.tachiyomi.data.track.anilist.model.OAuth +import okhttp3.FormBody +import okhttp3.OkHttpClient +import okhttp3.ResponseBody +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.* +import rx.Observable + +interface AnilistApi { + + companion object { + private const val clientId = "tachiyomi-hrtje" + private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C" + private const val clientUrl = "tachiyomi://anilist-auth" + private const val baseUrl = "https://anilist.co/api/" + + fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon() + .appendQueryParameter("grant_type", "authorization_code") + .appendQueryParameter("client_id", clientId) + .appendQueryParameter("redirect_uri", clientUrl) + .appendQueryParameter("response_type", "code") + .build() + + fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token", + body = FormBody.Builder() + .add("grant_type", "refresh_token") + .add("client_id", clientId) + .add("client_secret", clientSecret) + .add("refresh_token", token) + .build()) + + fun createService(client: OkHttpClient) = Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) + .build() + .create(AnilistApi::class.java) + + } + + @FormUrlEncoded + @POST("auth/access_token") + fun requestAccessToken( + @Field("code") code: String, + @Field("grant_type") grant_type: String = "authorization_code", + @Field("client_id") client_id: String = clientId, + @Field("client_secret") client_secret: String = clientSecret, + @Field("redirect_uri") redirect_uri: String = clientUrl) + : Observable + + @GET("user") + fun getCurrentUser(): Observable + + @GET("manga/search/{query}") + fun search(@Path("query") query: String, @Query("page") page: Int): Observable> + + @GET("user/{username}/mangalist") + fun getList(@Path("username") username: String): Observable + + @FormUrlEncoded + @PUT("mangalist") + fun addManga( + @Field("id") id: Int, + @Field("chapters_read") chapters_read: Int, + @Field("list_status") list_status: String) + : Observable> + + @FormUrlEncoded + @PUT("mangalist") + fun updateManga( + @Field("id") id: Int, + @Field("chapters_read") chapters_read: Int, + @Field("list_status") list_status: String, + @Field("score") score_raw: String) + : Observable> + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/AnilistInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt similarity index 91% rename from app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/AnilistInterceptor.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt index a7a1232f1..45b81864b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/AnilistInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt @@ -1,61 +1,61 @@ -package eu.kanade.tachiyomi.data.mangasync.anilist - -import com.google.gson.Gson -import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth -import okhttp3.Interceptor -import okhttp3.Response - -class AnilistInterceptor(private var refreshToken: String?) : Interceptor { - - /** - * OAuth object used for authenticated requests. - * - * Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute - * before its original expiration date. - */ - private var oauth: OAuth? = null - set(value) { - field = value?.copy(expires = value.expires * 1000 - 60 * 1000) - } - - override fun intercept(chain: Interceptor.Chain): Response { - val originalRequest = chain.request() - - if (refreshToken.isNullOrEmpty()) { - throw Exception("Not authenticated with Anilist") - } - - // Refresh access token if null or expired. - if (oauth == null || oauth!!.isExpired()) { - val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!)) - oauth = if (response.isSuccessful) { - Gson().fromJson(response.body().string(), OAuth::class.java) - } else { - response.close() - null - } - } - - // Throw on null auth. - if (oauth == null) { - throw Exception("Access token wasn't refreshed") - } - - // Add the authorization header to the original request. - val authRequest = originalRequest.newBuilder() - .addHeader("Authorization", "Bearer ${oauth!!.access_token}") - .build() - - return chain.proceed(authRequest) - } - - /** - * Called when the user authenticates with Anilist for the first time. Sets the refresh token - * and the oauth object. - */ - fun setAuth(oauth: OAuth?) { - refreshToken = oauth?.refresh_token - this.oauth = oauth - } - +package eu.kanade.tachiyomi.data.track.anilist + +import com.google.gson.Gson +import eu.kanade.tachiyomi.data.track.anilist.model.OAuth +import okhttp3.Interceptor +import okhttp3.Response + +class AnilistInterceptor(private var refreshToken: String?) : Interceptor { + + /** + * OAuth object used for authenticated requests. + * + * Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute + * before its original expiration date. + */ + private var oauth: OAuth? = null + set(value) { + field = value?.copy(expires = value.expires * 1000 - 60 * 1000) + } + + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + + if (refreshToken.isNullOrEmpty()) { + throw Exception("Not authenticated with Anilist") + } + + // Refresh access token if null or expired. + if (oauth == null || oauth!!.isExpired()) { + val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!)) + oauth = if (response.isSuccessful) { + Gson().fromJson(response.body().string(), OAuth::class.java) + } else { + response.close() + null + } + } + + // Throw on null auth. + if (oauth == null) { + throw Exception("Access token wasn't refreshed") + } + + // Add the authorization header to the original request. + val authRequest = originalRequest.newBuilder() + .addHeader("Authorization", "Bearer ${oauth!!.access_token}") + .build() + + return chain.proceed(authRequest) + } + + /** + * Called when the user authenticates with Anilist for the first time. Sets the refresh token + * and the oauth object. + */ + fun setAuth(oauth: OAuth?) { + refreshToken = oauth?.refresh_token + this.oauth = oauth + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/model/ALManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALManga.kt similarity index 51% rename from app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/model/ALManga.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALManga.kt index 7b4a64ebb..d63ffae00 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/model/ALManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALManga.kt @@ -1,17 +1,17 @@ -package eu.kanade.tachiyomi.data.mangasync.anilist.model - -import eu.kanade.tachiyomi.data.database.models.MangaSync -import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager - -data class ALManga( - val id: Int, - val title_romaji: String, - val type: String, - val total_chapters: Int) { - - fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply { - remote_id = this@ALManga.id - title = title_romaji - total_chapters = this@ALManga.total_chapters - } +package eu.kanade.tachiyomi.data.track.anilist.model + +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackManager + +data class ALManga( + val id: Int, + val title_romaji: String, + val type: String, + val total_chapters: Int) { + + fun toTrack() = Track.create(TrackManager.ANILIST).apply { + remote_id = this@ALManga.id + title = title_romaji + total_chapters = this@ALManga.total_chapters + } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/model/ALUserLists.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALUserLists.kt similarity index 65% rename from app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/model/ALUserLists.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALUserLists.kt index 7caf7def5..2e1018bcc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/model/ALUserLists.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALUserLists.kt @@ -1,6 +1,6 @@ -package eu.kanade.tachiyomi.data.mangasync.anilist.model - -data class ALUserLists(val lists: Map>) { - - fun flatten() = lists.values.flatten() +package eu.kanade.tachiyomi.data.track.anilist.model + +data class ALUserLists(val lists: Map>) { + + fun flatten() = lists.values.flatten() } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/model/ALUserManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALUserManga.kt similarity index 58% rename from app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/model/ALUserManga.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALUserManga.kt index 406ed8e01..6692a8b44 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/model/ALUserManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/ALUserManga.kt @@ -1,29 +1,29 @@ -package eu.kanade.tachiyomi.data.mangasync.anilist.model - -import eu.kanade.tachiyomi.data.database.models.MangaSync -import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager -import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist - -data class ALUserManga( - val id: Int, - val list_status: String, - val score_raw: Int, - val chapters_read: Int, - val manga: ALManga) { - - fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply { - remote_id = manga.id - status = getMangaSyncStatus() - score = score_raw.toFloat() - last_chapter_read = chapters_read - } - - fun getMangaSyncStatus() = when (list_status) { - "reading" -> Anilist.READING - "completed" -> Anilist.COMPLETED - "on-hold" -> Anilist.ON_HOLD - "dropped" -> Anilist.DROPPED - "plan to read" -> Anilist.PLAN_TO_READ - else -> throw NotImplementedError("Unknown status") - } +package eu.kanade.tachiyomi.data.track.anilist.model + +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.anilist.Anilist + +data class ALUserManga( + val id: Int, + val list_status: String, + val score_raw: Int, + val chapters_read: Int, + val manga: ALManga) { + + fun toTrack() = Track.create(TrackManager.ANILIST).apply { + remote_id = manga.id + status = toTrackStatus() + score = score_raw.toFloat() + last_chapter_read = chapters_read + } + + fun toTrackStatus() = when (list_status) { + "reading" -> Anilist.READING + "completed" -> Anilist.COMPLETED + "on-hold" -> Anilist.ON_HOLD + "dropped" -> Anilist.DROPPED + "plan to read" -> Anilist.PLAN_TO_READ + else -> throw NotImplementedError("Unknown status") + } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/model/OAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/OAuth.kt similarity index 78% rename from app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/model/OAuth.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/OAuth.kt index 05b21a83b..008e9a9f8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/anilist/model/OAuth.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/model/OAuth.kt @@ -1,11 +1,11 @@ -package eu.kanade.tachiyomi.data.mangasync.anilist.model - -data class OAuth( - val access_token: String, - val token_type: String, - val expires: Long, - val expires_in: Long, - val refresh_token: String?) { - - fun isExpired() = System.currentTimeMillis() > expires +package eu.kanade.tachiyomi.data.track.anilist.model + +data class OAuth( + val access_token: String, + val token_type: String, + val expires: Long, + val expires_in: Long, + val refresh_token: String?) { + + fun isExpired() = System.currentTimeMillis() > expires } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt new file mode 100644 index 000000000..c9de3bac3 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt @@ -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> { + 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 { + 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 { + return api.findLibManga(getUserId(), track.remote_id) + .map { json -> + val data = json["data"].array + if (data.size() > 0) { + KitsuLibManga(data[0].obj, json["included"].array[0].obj).toTrack() + } else { + null + } + } + } + + override fun add(track: Track): Observable { + // @formatter:off + val data = jsonObject( + "type" to "libraryEntries", + "attributes" to jsonObject( + "status" to track.getKitsuStatus(), + "progress" to track.last_chapter_read + ), + "relationships" to jsonObject( + "user" to jsonObject( + "data" to jsonObject( + "id" to getUserId(), + "type" to "users" + ) + ), + "media" to jsonObject( + "data" to jsonObject( + "id" to track.remote_id, + "type" to "manga" + ) + ) + ) + ) + // @formatter:on + + return api.addLibManga(jsonObject("data" to data)) + .doOnNext { json -> track.remote_id = json["data"]["id"].int } + .doOnError { Timber.e(it) } + .map { track } + } + + override fun update(track: Track): Observable { + if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { + track.status = COMPLETED + } + // @formatter:off + val data = jsonObject( + "type" to "libraryEntries", + "id" to track.remote_id, + "attributes" to jsonObject( + "status" to track.getKitsuStatus(), + "progress" to track.last_chapter_read, + "rating" to track.getKitsuScore() + ) + ) + // @formatter:on + + return api.updateLibManga(track.remote_id, jsonObject("data" to data)) + .map { track } + } + + override fun refresh(track: Track): Observable { + return api.getLibManga(track.remote_id) + .map { json -> + val data = json["data"].array + if (data.size() > 0) { + val include = json["included"].array[0].obj + val remoteTrack = KitsuLibManga(data[0].obj, include).toTrack() + track.copyPersonalFrom(remoteTrack) + track.total_chapters = remoteTrack.total_chapters + track + } else { + throw Exception("Could not find manga") + } + } + } + + override fun getStatusList(): List { + return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ) + } + + override fun getStatus(status: Int): String = with(context) { + when (status) { + READING -> getString(R.string.reading) + COMPLETED -> getString(R.string.completed) + ON_HOLD -> getString(R.string.on_hold) + DROPPED -> getString(R.string.dropped) + PLAN_TO_READ -> getString(R.string.plan_to_read) + else -> "" + } + } + + private fun Track.getKitsuStatus() = when (status) { + READING -> "current" + COMPLETED -> "completed" + ON_HOLD -> "on_hold" + DROPPED -> "dropped" + PLAN_TO_READ -> "planned" + else -> throw Exception("Unknown status") + } + + private fun Track.getKitsuScore(): String { + return if (score > 0) (score / 2).toString() else "" + } + + override fun getLogo(): Int { + return R.drawable.kitsu + } + + override fun getLogoColor(): Int { + return Color.rgb(51, 37, 50) + } + + override fun maxScore(): Int { + return 10 + } + + override fun formatScore(track: Track): String { + return track.getKitsuScore() + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt new file mode 100644 index 000000000..8ab8cdbda --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -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 + + @GET("users") + fun getCurrentUser( + @Query("filter[self]", encoded = true) self: Boolean = true + ) : Observable + + @GET("manga") + fun search( + @Query("filter[text]", encoded = true) query: String + ): Observable + + @GET("library-entries") + fun getLibManga( + @Query("filter[id]", encoded = true) remoteId: Int, + @Query("include") includes: String = "media" + ) : Observable + + @GET("library-entries") + fun findLibManga( + @Query("filter[user_id]", encoded = true) userId: String, + @Query("filter[media_id]", encoded = true) remoteId: Int, + @Query("page[limit]", encoded = true) limit: Int = 10000, + @Query("include") includes: String = "media" + ) : Observable + + @Headers("Content-Type: application/vnd.api+json") + @POST("library-entries") + fun addLibManga( + @Body data: JsonObject + ) : Observable + + @Headers("Content-Type: application/vnd.api+json") + @PATCH("library-entries/{id}") + fun updateLibManga( + @Path("id") remoteId: Int, + @Body data: JsonObject + ) : Observable + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt new file mode 100644 index 000000000..92847310f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt @@ -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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt new file mode 100644 index 000000000..706674962 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt @@ -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") + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/OAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/OAuth.kt new file mode 100644 index 000000000..e9f2ae401 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/OAuth.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt similarity index 53% rename from app/src/main/java/eu/kanade/tachiyomi/data/mangasync/myanimelist/MyAnimeList.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt index 66a475cf5..b0e25261c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/myanimelist/MyAnimeList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt @@ -1,222 +1,263 @@ -package eu.kanade.tachiyomi.data.mangasync.myanimelist - -import android.content.Context -import android.net.Uri -import android.util.Xml -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.MangaSync -import eu.kanade.tachiyomi.data.mangasync.MangaSyncService -import eu.kanade.tachiyomi.data.network.GET -import eu.kanade.tachiyomi.data.network.POST -import eu.kanade.tachiyomi.data.network.asObservable -import eu.kanade.tachiyomi.util.selectInt -import eu.kanade.tachiyomi.util.selectText -import okhttp3.Credentials -import okhttp3.FormBody -import okhttp3.Headers -import okhttp3.RequestBody -import org.jsoup.Jsoup -import org.xmlpull.v1.XmlSerializer -import rx.Completable -import rx.Observable -import java.io.StringWriter - -class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) { - - private lateinit var headers: Headers - - companion object { - val BASE_URL = "https://myanimelist.net" - - private val ENTRY_TAG = "entry" - private val CHAPTER_TAG = "chapter" - private val SCORE_TAG = "score" - private val STATUS_TAG = "status" - - val READING = 1 - val COMPLETED = 2 - val ON_HOLD = 3 - val DROPPED = 4 - val PLAN_TO_READ = 6 - - val DEFAULT_STATUS = READING - val DEFAULT_SCORE = 0 - } - - init { - val username = getUsername() - val password = getPassword() - - if (!username.isEmpty() && !password.isEmpty()) { - createHeaders(username, password) - } - } - - override val name: String - get() = "MyAnimeList" - - fun getLoginUrl() = Uri.parse(BASE_URL).buildUpon() - .appendEncodedPath("api/account/verify_credentials.xml") - .toString() - - fun getSearchUrl(query: String) = Uri.parse(BASE_URL).buildUpon() - .appendEncodedPath("api/manga/search.xml") - .appendQueryParameter("q", query) - .toString() - - fun getListUrl(username: String) = Uri.parse(BASE_URL).buildUpon() - .appendPath("malappinfo.php") - .appendQueryParameter("u", username) - .appendQueryParameter("status", "all") - .appendQueryParameter("type", "manga") - .toString() - - fun getUpdateUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon() - .appendEncodedPath("api/mangalist/update") - .appendPath("${manga.remote_id}.xml") - .toString() - - fun getAddUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon() - .appendEncodedPath("api/mangalist/add") - .appendPath("${manga.remote_id}.xml") - .toString() - - override fun login(username: String, password: String): Completable { - createHeaders(username, password) - return client.newCall(GET(getLoginUrl(), headers)) - .asObservable() - .doOnNext { it.close() } - .doOnNext { if (it.code() != 200) throw Exception("Login error") } - .toCompletable() - } - - fun search(query: String): Observable> { - return client.newCall(GET(getSearchUrl(query), headers)) - .asObservable() - .map { Jsoup.parse(it.body().string()) } - .flatMap { Observable.from(it.select("entry")) } - .filter { it.select("type").text() != "Novel" } - .map { - MangaSync.create(id).apply { - title = it.selectText("title")!! - remote_id = it.selectInt("id") - total_chapters = it.selectInt("chapters") - } - } - .toList() - } - - // MAL doesn't support score with decimals - fun getList(): Observable> { - return networkService.forceCacheClient - .newCall(GET(getListUrl(getUsername()), headers)) - .asObservable() - .map { Jsoup.parse(it.body().string()) } - .flatMap { Observable.from(it.select("manga")) } - .map { - MangaSync.create(id).apply { - title = it.selectText("series_title")!! - remote_id = it.selectInt("series_mangadb_id") - last_chapter_read = it.selectInt("my_read_chapters") - status = it.selectInt("my_status") - score = it.selectInt("my_score").toFloat() - total_chapters = it.selectInt("series_chapters") - } - } - .toList() - } - - override fun update(manga: MangaSync): Observable { - return Observable.defer { - if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) { - manga.status = COMPLETED - } - client.newCall(POST(getUpdateUrl(manga), headers, getMangaPostPayload(manga))) - .asObservable() - .doOnNext { it.close() } - .doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") } - .map { manga } - } - - } - - override fun add(manga: MangaSync): Observable { - return Observable.defer { - client.newCall(POST(getAddUrl(manga), headers, getMangaPostPayload(manga))) - .asObservable() - .doOnNext { it.close() } - .doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") } - .map { manga } - } - } - - private fun getMangaPostPayload(manga: MangaSync): RequestBody { - val xml = Xml.newSerializer() - val writer = StringWriter() - - with(xml) { - setOutput(writer) - startDocument("UTF-8", false) - startTag("", ENTRY_TAG) - - // Last chapter read - if (manga.last_chapter_read != 0) { - inTag(CHAPTER_TAG, manga.last_chapter_read.toString()) - } - // Manga status in the list - inTag(STATUS_TAG, manga.status.toString()) - - // Manga score - inTag(SCORE_TAG, manga.score.toString()) - - endTag("", ENTRY_TAG) - endDocument() - } - - val form = FormBody.Builder() - form.add("data", writer.toString()) - return form.build() - } - - fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") { - startTag(namespace, tag) - text(body) - endTag(namespace, tag) - } - - override fun bind(manga: MangaSync): Observable { - return getList() - .flatMap { userlist -> - manga.sync_id = id - val mangaFromList = userlist.find { it.remote_id == manga.remote_id } - if (mangaFromList != null) { - manga.copyPersonalFrom(mangaFromList) - update(manga) - } else { - // Set default fields if it's not found in the list - manga.score = DEFAULT_SCORE.toFloat() - manga.status = DEFAULT_STATUS - add(manga) - } - } - } - - override fun getStatus(status: Int): String = with(context) { - when (status) { - READING -> getString(R.string.reading) - COMPLETED -> getString(R.string.completed) - ON_HOLD -> getString(R.string.on_hold) - DROPPED -> getString(R.string.dropped) - PLAN_TO_READ -> getString(R.string.plan_to_read) - else -> "" - } - } - - fun createHeaders(username: String, password: String) { - val builder = Headers.Builder() - builder.add("Authorization", Credentials.basic(username, password)) - builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C") - headers = builder.build() - } - -} +package eu.kanade.tachiyomi.data.track.myanimelist + +import android.content.Context +import android.graphics.Color +import android.net.Uri +import android.util.Xml +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.network.GET +import eu.kanade.tachiyomi.data.network.POST +import eu.kanade.tachiyomi.data.network.asObservable +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.util.selectInt +import eu.kanade.tachiyomi.util.selectText +import okhttp3.Credentials +import okhttp3.FormBody +import okhttp3.Headers +import okhttp3.RequestBody +import org.jsoup.Jsoup +import org.xmlpull.v1.XmlSerializer +import rx.Completable +import rx.Observable +import java.io.StringWriter + +class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { + + private lateinit var headers: Headers + + companion object { + const val BASE_URL = "https://myanimelist.net" + + private val ENTRY_TAG = "entry" + private val CHAPTER_TAG = "chapter" + private val SCORE_TAG = "score" + private val STATUS_TAG = "status" + + const val READING = 1 + const val COMPLETED = 2 + const val ON_HOLD = 3 + const val DROPPED = 4 + const val PLAN_TO_READ = 6 + + const val DEFAULT_STATUS = READING + const val DEFAULT_SCORE = 0 + + const val PREFIX_MY = "my:" + } + + init { + val username = getUsername() + val password = getPassword() + + if (!username.isEmpty() && !password.isEmpty()) { + createHeaders(username, password) + } + } + + override val name: String + get() = "MyAnimeList" + + override fun getLogo() = R.drawable.mal + + override fun getLogoColor() = Color.rgb(46, 81, 162) + + override fun maxScore() = 10 + + override fun formatScore(track: Track): String { + return track.score.toInt().toString() + } + + fun getLoginUrl() = Uri.parse(BASE_URL).buildUpon() + .appendEncodedPath("api/account/verify_credentials.xml") + .toString() + + fun getSearchUrl(query: String) = Uri.parse(BASE_URL).buildUpon() + .appendEncodedPath("api/manga/search.xml") + .appendQueryParameter("q", query) + .toString() + + fun getListUrl(username: String) = Uri.parse(BASE_URL).buildUpon() + .appendPath("malappinfo.php") + .appendQueryParameter("u", username) + .appendQueryParameter("status", "all") + .appendQueryParameter("type", "manga") + .toString() + + fun getUpdateUrl(track: Track) = Uri.parse(BASE_URL).buildUpon() + .appendEncodedPath("api/mangalist/update") + .appendPath("${track.remote_id}.xml") + .toString() + + fun getAddUrl(track: Track) = Uri.parse(BASE_URL).buildUpon() + .appendEncodedPath("api/mangalist/add") + .appendPath("${track.remote_id}.xml") + .toString() + + override fun login(username: String, password: String): Completable { + createHeaders(username, password) + return client.newCall(GET(getLoginUrl(), headers)) + .asObservable() + .doOnNext { it.close() } + .doOnNext { if (it.code() != 200) throw Exception("Login error") } + .doOnNext { saveCredentials(username, password) } + .doOnError { logout() } + .toCompletable() + } + + override fun search(query: String): Observable> { + return if (query.startsWith(PREFIX_MY)) { + val realQuery = query.substring(PREFIX_MY.length).toLowerCase().trim() + getList() + .flatMap { Observable.from(it) } + .filter { realQuery in it.title.toLowerCase() } + .toList() + } else { + client.newCall(GET(getSearchUrl(query), headers)) + .asObservable() + .map { Jsoup.parse(it.body().string()) } + .flatMap { Observable.from(it.select("entry")) } + .filter { it.select("type").text() != "Novel" } + .map { + Track.create(id).apply { + title = it.selectText("title")!! + remote_id = it.selectInt("id") + total_chapters = it.selectInt("chapters") + } + } + .toList() + } + } + + override fun refresh(track: Track): Observable { + return getList() + .map { myList -> + val remoteTrack = myList.find { it.remote_id == track.remote_id } + if (remoteTrack != null) { + track.copyPersonalFrom(remoteTrack) + track.total_chapters = remoteTrack.total_chapters + track + } else { + throw Exception("Could not find manga") + } + } + } + + // MAL doesn't support score with decimals + fun getList(): Observable> { + return networkService.forceCacheClient + .newCall(GET(getListUrl(getUsername()), headers)) + .asObservable() + .map { Jsoup.parse(it.body().string()) } + .flatMap { Observable.from(it.select("manga")) } + .map { + Track.create(id).apply { + title = it.selectText("series_title")!! + remote_id = it.selectInt("series_mangadb_id") + last_chapter_read = it.selectInt("my_read_chapters") + status = it.selectInt("my_status") + score = it.selectInt("my_score").toFloat() + total_chapters = it.selectInt("series_chapters") + } + } + .toList() + } + + override fun update(track: Track): Observable { + return Observable.defer { + if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) { + track.status = COMPLETED + } + client.newCall(POST(getUpdateUrl(track), headers, getMangaPostPayload(track))) + .asObservable() + .doOnNext { it.close() } + .doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") } + .map { track } + } + + } + + override fun add(track: Track): Observable { + return Observable.defer { + client.newCall(POST(getAddUrl(track), headers, getMangaPostPayload(track))) + .asObservable() + .doOnNext { it.close() } + .doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") } + .map { track } + } + } + + private fun getMangaPostPayload(track: Track): RequestBody { + val xml = Xml.newSerializer() + val writer = StringWriter() + + with(xml) { + setOutput(writer) + startDocument("UTF-8", false) + startTag("", ENTRY_TAG) + + // Last chapter read + if (track.last_chapter_read != 0) { + inTag(CHAPTER_TAG, track.last_chapter_read.toString()) + } + // Manga status in the list + inTag(STATUS_TAG, track.status.toString()) + + // Manga score + inTag(SCORE_TAG, track.score.toString()) + + endTag("", ENTRY_TAG) + endDocument() + } + + val form = FormBody.Builder() + form.add("data", writer.toString()) + return form.build() + } + + fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") { + startTag(namespace, tag) + text(body) + endTag(namespace, tag) + } + + override fun bind(track: Track): Observable { + 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 { + 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() + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt index 194426ddf..efada0dc3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt @@ -3,15 +3,18 @@ package eu.kanade.tachiyomi.ui.manga import android.content.Context import android.content.Intent import android.os.Bundle +import android.support.graphics.drawable.VectorDrawableCompat import android.support.v4.app.Fragment import android.support.v4.app.FragmentManager import android.support.v4.app.FragmentPagerAdapter +import android.widget.LinearLayout +import android.widget.TextView import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment 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.toast import kotlinx.android.synthetic.main.activity_manga.* @@ -28,7 +31,7 @@ class MangaActivity : BaseRxActivity() { const val FROM_LAUNCHER_EXTRA = "from_launcher" const val INFO_FRAGMENT = 0 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 { SharedData.put(MangaEvent(manga)) @@ -71,6 +74,7 @@ class MangaActivity : BaseRxActivity() { fromCatalogue = intent.getBooleanExtra(FROM_CATALOGUE_EXTRA, false) adapter = MangaDetailAdapter(supportFragmentManager, this) + view_pager.offscreenPageLimit = 3 view_pager.adapter = adapter tabs.setupWithViewPager(view_pager) @@ -85,33 +89,50 @@ class MangaActivity : BaseRxActivity() { 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 - private val tabTitles = arrayOf(activity.getString(R.string.manga_detail_tab), - activity.getString(R.string.manga_chapters_tab), "MAL") + // I had no choice but to use reflection... + val field = tab.javaClass.getDeclaredField("mView").apply { isAccessible = true } + 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 { - pageCount = 2 - if (!activity.fromCatalogue && activity.presenter.syncManager.myAnimeList.isLogged) - pageCount++ + if (!activity.fromCatalogue && activity.presenter.trackManager.hasLoggedServices()) + tabCount++ } override fun getCount(): Int { - return pageCount + return tabCount } - override fun getItem(position: Int): Fragment? { + override fun getItem(position: Int): Fragment { when (position) { INFO_FRAGMENT -> return MangaInfoFragment.newInstance() CHAPTERS_FRAGMENT -> return ChaptersFragment.newInstance() - MYANIMELIST_FRAGMENT -> return MyAnimeListFragment.newInstance() - else -> return null + TRACK_FRAGMENT -> return TrackFragment.newInstance() + else -> throw Exception("Unknown position") } } override fun getPageTitle(position: Int): CharSequence { - // Generate title based on item position return tabTitles[position] } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index b322360e1..022ee880b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.manga import android.os.Bundle import eu.kanade.tachiyomi.data.database.DatabaseHelper 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.manga.info.ChapterCountEvent import eu.kanade.tachiyomi.util.SharedData @@ -22,9 +22,9 @@ class MangaPresenter : BasePresenter() { 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. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListDialogFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListDialogFragment.kt deleted file mode 100644 index 9cccdcb8e..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListDialogFragment.kt +++ /dev/null @@ -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 - 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() - - 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) { - 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 - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListFragment.kt deleted file mode 100644 index 170a8ffb9..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListFragment.kt +++ /dev/null @@ -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() { - - 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) { - 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() - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListPresenter.kt deleted file mode 100644 index 248d04a96..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListPresenter.kt +++ /dev/null @@ -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() { - - 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> { - return query?.let { query -> - val observable: Observable> - 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 { - 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 { - 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) - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListSearchAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListSearchAdapter.kt deleted file mode 100644 index 8ce5207d1..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListSearchAdapter.kt +++ /dev/null @@ -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(context, R.layout.dialog_myanimelist_search_item, ArrayList()) { - - 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) { - setNotifyOnChange(false) - clear() - addAll(syncs) - notifyDataSetChanged() - } - - class SearchViewHolder(private val view: View) { - - fun onSetValues(sync: MangaSync) { - view.myanimelist_result_title.text = sync.title - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt new file mode 100644 index 000000000..08e727b97 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt @@ -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() { + + var items = emptyList() + 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]) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackFragment.kt new file mode 100644 index 000000000..10e47917a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackFragment.kt @@ -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() { + + 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) { + adapter.items = trackings + swipe_refresh.isEnabled = trackings.any { it.track != null } + (activity as MangaActivity).setTrackingIcon(trackings.any { it.track != null }) + } + + fun onSearchResults(results: List) { + 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() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt new file mode 100644 index 000000000..9a9271dd2 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt @@ -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 = "" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt new file mode 100644 index 000000000..9a435cd32 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt @@ -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) { + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt new file mode 100644 index 000000000..6988c299b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt @@ -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() { + + private val db: DatabaseHelper by injectLazy() + + private val trackManager: TrackManager by injectLazy() + + lateinit var manga: Manga + private set + + private var trackList: List = 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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt new file mode 100644 index 000000000..7aa5f7653 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt @@ -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(context, R.layout.item_track_search, ArrayList()) { + + 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) { + setNotifyOnChange(false) + clear() + addAll(syncs) + notifyDataSetChanged() + } + + class TrackSearchHolder(private val view: View) { + + fun onSetValues(track: Track) { + view.track_search_title.text = track.title + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt new file mode 100644 index 000000000..787180600 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt @@ -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() } + + 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) { + 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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index f77aa8f97..6afb528d1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -163,19 +163,19 @@ class ReaderActivity : BaseRxActivity() { } override fun onBackPressed() { - val chapterToUpdate = presenter.getMangaSyncChapterToUpdate() + val chapterToUpdate = presenter.getTrackChapterToUpdate() if (chapterToUpdate > 0) { - if (preferences.askUpdateMangaSync()) { + if (preferences.askUpdateTrack()) { MaterialDialog.Builder(this) .content(getString(R.string.confirm_update_manga_sync, chapterToUpdate)) .positiveText(android.R.string.yes) .negativeText(android.R.string.no) - .onPositive { dialog, which -> presenter.updateMangaSyncLastChapterRead() } + .onPositive { dialog, which -> presenter.updateTrackLastChapterRead() } .onAny { dialog1, which1 -> super.onBackPressed() } .show() } else { - presenter.updateMangaSyncLastChapterRead() + presenter.updateTrackLastChapterRead() super.onBackPressed() } } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index 151ff02a2..f85ab3665 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -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.History 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.mangasync.MangaSyncManager -import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.model.Page 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.reader.notification.ImageNotifier import eu.kanade.tachiyomi.util.DiskUtil @@ -54,9 +54,9 @@ class ReaderPresenter : BasePresenter() { val downloadManager: DownloadManager by injectLazy() /** - * Sync manager. + * Tracking manager. */ - val syncManager: MangaSyncManager by injectLazy() + val trackManager: TrackManager by injectLazy() /** * Source manager. @@ -124,7 +124,7 @@ class ReaderPresenter : BasePresenter() { /** * List of manga services linked to the active manga, or null if auto syncing is not enabled. */ - private var mangaSyncList: List? = null + private var trackList: List? = null /** * Chapter loader whose job is to obtain the chapter list and initialize every page. @@ -165,9 +165,9 @@ class ReaderPresenter : BasePresenter() { .subscribeLatestCache({ view, manga -> view.onMangaOpen(manga) }) // Retrieve the sync list if auto syncing is enabled. - if (prefs.autoUpdateMangaSync()) { - add(db.getMangasSync(manga).asRxSingle() - .subscribe({ mangaSyncList = it })) + if (prefs.autoUpdateTrack()) { + add(db.getTracks(manga).asRxSingle() + .subscribe({ trackList = it })) } restartableLatestCache(LOAD_ACTIVE_CHAPTER, @@ -431,9 +431,9 @@ class ReaderPresenter : BasePresenter() { /** * Returns the chapter to be marked as last read in sync services or 0 if no update required. */ - fun getMangaSyncChapterToUpdate(): Int { - val mangaSyncList = mangaSyncList - if (chapter.pages == null || mangaSyncList == null || mangaSyncList.isEmpty()) + fun getTrackChapterToUpdate(): Int { + val trackList = trackList + if (chapter.pages == null || trackList == null || trackList.isEmpty()) return 0 val prevChapter = prevChapter @@ -446,24 +446,24 @@ class ReaderPresenter : BasePresenter() { else 0 - mangaSyncList.forEach { sync -> + trackList.forEach { sync -> if (lastChapterRead > sync.last_chapter_read) { sync.last_chapter_read = lastChapterRead 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 */ - fun updateMangaSyncLastChapterRead() { - mangaSyncList?.forEach { sync -> - val service = syncManager.getService(sync.sync_id) + fun updateTrackLastChapterRead() { + trackList?.forEach { sync -> + val service = trackManager.getService(sync.sync_id) if (service != null && service.isLogged && sync.update) { - UpdateMangaSyncService.start(context, sync) + TrackUpdateService.start(context, sync) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AnilistLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AnilistLoginActivity.kt index c7209e370..b49282531 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AnilistLoginActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AnilistLoginActivity.kt @@ -7,14 +7,14 @@ import android.view.Gravity.CENTER import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.widget.FrameLayout 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.schedulers.Schedulers import uy.kohesive.injekt.injectLazy class AnilistLoginActivity : AppCompatActivity() { - private val syncManager: MangaSyncManager by injectLazy() + private val trackManager: TrackManager by injectLazy() override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) @@ -24,7 +24,7 @@ class AnilistLoginActivity : AppCompatActivity() { val code = intent.data?.getQueryParameter("code") if (code != null) { - syncManager.aniList.login(code) + trackManager.aniList.login(code) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ @@ -33,7 +33,7 @@ class AnilistLoginActivity : AppCompatActivity() { returnToSettings() }) } else { - syncManager.aniList.logout() + trackManager.aniList.logout() returnToSettings() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsActivity.kt index 476b9659e..449cb9ff0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsActivity.kt @@ -63,7 +63,7 @@ class SettingsActivity : BaseActivity(), "general_screen" -> SettingsGeneralFragment.newInstance(key) "downloads_screen" -> SettingsDownloadsFragment.newInstance(key) "sources_screen" -> SettingsSourcesFragment.newInstance(key) - "sync_screen" -> SettingsSyncFragment.newInstance(key) + "tracking_screen" -> SettingsTrackingFragment.newInstance(key) "advanced_screen" -> SettingsAdvancedFragment.newInstance(key) "about_screen" -> SettingsAboutFragment.newInstance(key) else -> SettingsFragment.newInstance(key) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt index 1439da6e8..9db02ad00 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt @@ -28,7 +28,7 @@ open class SettingsFragment : XpPreferenceFragment() { addPreferencesFromResource(R.xml.pref_reader) addPreferencesFromResource(R.xml.pref_downloads) addPreferencesFromResource(R.xml.pref_sources) - addPreferencesFromResource(R.xml.pref_sync) + addPreferencesFromResource(R.xml.pref_tracking) addPreferencesFromResource(R.xml.pref_advanced) addPreferencesFromResource(R.xml.pref_about) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSyncFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingFragment.kt similarity index 53% rename from app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSyncFragment.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingFragment.kt index e701056fa..d6948d7c6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSyncFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingFragment.kt @@ -1,89 +1,94 @@ -package eu.kanade.tachiyomi.ui.setting - -import android.content.Intent -import android.os.Bundle -import android.support.v7.preference.PreferenceCategory -import android.support.v7.preference.XpPreferenceFragment -import android.view.View -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager -import eu.kanade.tachiyomi.data.mangasync.MangaSyncService -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.widget.preference.LoginPreference -import eu.kanade.tachiyomi.widget.preference.MangaSyncLoginDialog -import uy.kohesive.injekt.injectLazy - -class SettingsSyncFragment : SettingsFragment() { - - companion object { - const val SYNC_CHANGE_REQUEST = 121 - - fun newInstance(rootKey: String): SettingsSyncFragment { - val args = Bundle() - args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey) - return SettingsSyncFragment().apply { arguments = args } - } - } - - private val syncManager: MangaSyncManager by injectLazy() - - private val preferences: PreferencesHelper by injectLazy() - - val syncCategory: PreferenceCategory by bindPref(R.string.pref_category_manga_sync_accounts_key) - - override fun onViewCreated(view: View, savedState: Bundle?) { - super.onViewCreated(view, savedState) - - registerService(syncManager.myAnimeList) - -// registerService(syncManager.aniList) { -// val intent = CustomTabsIntent.Builder() -// .setToolbarColor(activity.theme.getResourceColor(R.attr.colorPrimary)) -// .build() -// intent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) -// intent.launchUrl(activity, AnilistApi.authUrl()) -// } - } - - private fun registerService( - service: T, - onPreferenceClick: (T) -> Unit = defaultOnPreferenceClick) { - - LoginPreference(preferenceManager.context).apply { - key = preferences.keys.syncUsername(service.id) - title = service.name - - setOnPreferenceClickListener { - onPreferenceClick(service) - true - } - - syncCategory.addPreference(this) - } - } - - private val defaultOnPreferenceClick: (MangaSyncService) -> Unit - get() = { - val fragment = MangaSyncLoginDialog.newInstance(it) - fragment.setTargetFragment(this, SYNC_CHANGE_REQUEST) - fragment.show(fragmentManager, null) - } - - override fun onResume() { - super.onResume() - // Manually refresh anilist holder -// updatePreference(syncManager.aniList.id) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == SYNC_CHANGE_REQUEST) { - updatePreference(resultCode) - } - } - - private fun updatePreference(id: Int) { - val pref = findPreference(preferences.keys.syncUsername(id)) as? LoginPreference - pref?.notifyChanged() - } - -} +package eu.kanade.tachiyomi.ui.setting + +import android.content.Intent +import android.os.Bundle +import android.support.customtabs.CustomTabsIntent +import android.support.v7.preference.PreferenceCategory +import android.support.v7.preference.XpPreferenceFragment +import android.view.View +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.anilist.AnilistApi +import eu.kanade.tachiyomi.util.getResourceColor +import eu.kanade.tachiyomi.widget.preference.LoginPreference +import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog +import uy.kohesive.injekt.injectLazy + +class SettingsTrackingFragment : SettingsFragment() { + + companion object { + const val SYNC_CHANGE_REQUEST = 121 + + fun newInstance(rootKey: String): SettingsTrackingFragment { + val args = Bundle() + args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey) + return SettingsTrackingFragment().apply { arguments = args } + } + } + + private val trackManager: TrackManager by injectLazy() + + private val preferences: PreferencesHelper by injectLazy() + + val syncCategory: PreferenceCategory by bindPref(R.string.pref_category_tracking_accounts_key) + + override fun onViewCreated(view: View, savedState: Bundle?) { + super.onViewCreated(view, savedState) + + registerService(trackManager.myAnimeList) + + registerService(trackManager.aniList) { + val intent = CustomTabsIntent.Builder() + .setToolbarColor(activity.theme.getResourceColor(R.attr.colorPrimary)) + .build() + intent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) + intent.launchUrl(activity, AnilistApi.authUrl()) + } + + registerService(trackManager.kitsu) + } + + private fun registerService( + service: T, + onPreferenceClick: (T) -> Unit = defaultOnPreferenceClick) { + + LoginPreference(preferenceManager.context).apply { + key = preferences.keys.trackUsername(service.id) + title = service.name + + setOnPreferenceClickListener { + onPreferenceClick(service) + true + } + + syncCategory.addPreference(this) + } + } + + private val defaultOnPreferenceClick: (TrackService) -> Unit + get() = { + val fragment = TrackLoginDialog.newInstance(it) + fragment.setTargetFragment(this, SYNC_CHANGE_REQUEST) + fragment.show(fragmentManager, null) + } + + override fun onResume() { + super.onResume() + // Manually refresh anilist holder + updatePreference(trackManager.aniList.id) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == SYNC_CHANGE_REQUEST) { + updatePreference(resultCode) + } + } + + private fun updatePreference(id: Int) { + val pref = findPreference(preferences.keys.trackUsername(id)) as? LoginPreference + pref?.notifyChanged() + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/MangaSyncLoginDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt similarity index 76% rename from app/src/main/java/eu/kanade/tachiyomi/widget/preference/MangaSyncLoginDialog.kt rename to app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt index 49b46da4b..fb6354dc8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/MangaSyncLoginDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt @@ -3,20 +3,20 @@ package eu.kanade.tachiyomi.widget.preference import android.os.Bundle import android.view.View import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager -import eu.kanade.tachiyomi.data.mangasync.MangaSyncService +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.util.toast import kotlinx.android.synthetic.main.pref_account_login.view.* import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import uy.kohesive.injekt.injectLazy -class MangaSyncLoginDialog : LoginDialogPreference() { +class TrackLoginDialog : LoginDialogPreference() { companion object { - fun newInstance(sync: MangaSyncService): LoginDialogPreference { - val fragment = MangaSyncLoginDialog() + fun newInstance(sync: TrackService): LoginDialogPreference { + val fragment = TrackLoginDialog() val bundle = Bundle(1) bundle.putInt("key", sync.id) 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?) { super.onCreate(savedInstanceState) val syncId = arguments.getInt("key") - sync = syncManager.getService(syncId)!! + sync = trackManager.getService(syncId)!! } override fun setCredentialsOnView(view: View) = with(view) { @@ -56,11 +56,9 @@ class MangaSyncLoginDialog : LoginDialogPreference() { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ - sync.saveCredentials(user, pass) dialog.dismiss() context.toast(R.string.login_success) }, { error -> - sync.logout() login.progress = -1 login.setText(R.string.unknown_error) }) diff --git a/app/src/main/res/drawable-xxxhdpi/al.png b/app/src/main/res/drawable-xxxhdpi/al.png new file mode 100644 index 000000000..6529ad678 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/al.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/kitsu.png b/app/src/main/res/drawable-xxxhdpi/kitsu.png new file mode 100644 index 000000000..bb9caec08 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/kitsu.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/mal.png b/app/src/main/res/drawable-xxxhdpi/mal.png new file mode 100644 index 000000000..b148f40be Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/mal.png differ diff --git a/app/src/main/res/drawable/ic_done_white_18dp.xml b/app/src/main/res/drawable/ic_done_white_18dp.xml new file mode 100644 index 000000000..3bd793040 --- /dev/null +++ b/app/src/main/res/drawable/ic_done_white_18dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/card_myanimelist_personal.xml b/app/src/main/res/layout/card_myanimelist_personal.xml deleted file mode 100644 index 45f572839..000000000 --- a/app/src/main/res/layout/card_myanimelist_personal.xml +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_myanimelist_search_item.xml b/app/src/main/res/layout/dialog_myanimelist_search_item.xml deleted file mode 100644 index 9b8cbeee8..000000000 --- a/app/src/main/res/layout/dialog_myanimelist_search_item.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_myanimelist_chapters.xml b/app/src/main/res/layout/dialog_track_chapters.xml similarity index 100% rename from app/src/main/res/layout/dialog_myanimelist_chapters.xml rename to app/src/main/res/layout/dialog_track_chapters.xml diff --git a/app/src/main/res/layout/dialog_myanimelist_score.xml b/app/src/main/res/layout/dialog_track_score.xml similarity index 100% rename from app/src/main/res/layout/dialog_myanimelist_score.xml rename to app/src/main/res/layout/dialog_track_score.xml diff --git a/app/src/main/res/layout/dialog_myanimelist_search.xml b/app/src/main/res/layout/dialog_track_search.xml similarity index 89% rename from app/src/main/res/layout/dialog_myanimelist_search.xml rename to app/src/main/res/layout/dialog_track_search.xml index 00b2abf23..11d4886a5 100644 --- a/app/src/main/res/layout/dialog_myanimelist_search.xml +++ b/app/src/main/res/layout/dialog_track_search.xml @@ -14,11 +14,11 @@ android:paddingRight="@dimen/margin_right"> + android:hint="@string/title"/> @@ -33,7 +33,7 @@ android:visibility="gone"/> - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_track.xml b/app/src/main/res/layout/fragment_track.xml new file mode 100644 index 000000000..b73a8cad6 --- /dev/null +++ b/app/src/main/res/layout/fragment_track.xml @@ -0,0 +1,20 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_track.xml b/app/src/main/res/layout/item_track.xml new file mode 100644 index 000000000..4260f4c0e --- /dev/null +++ b/app/src/main/res/layout/item_track.xml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_track_search.xml b/app/src/main/res/layout/item_track_search.xml new file mode 100644 index 000000000..ee0965eac --- /dev/null +++ b/app/src/main/res/layout/item_track_search.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 0f040da00..efb13c44f 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -65,7 +65,7 @@ Lector Descargas Fuentes - Sincronización + Seguimiento Avanzado Acerca de @@ -232,7 +232,7 @@ En espera Para leer luego Puntuación - Título… + Título Estado Capítulos diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index e825ef19a..cb2df4d23 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -240,7 +240,7 @@ Em espera Planeada a leitura Avaliação - Título… + Título Estado Capítulos diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index fd93e4f05..5ea2c173f 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -3,7 +3,7 @@ pref_category_general_key pref_category_reader_key - pref_category_sync_key + pref_category_tracking_key pref_category_downloads_key pref_category_advanced_key pref_category_about_key @@ -52,7 +52,7 @@ last_used_category pref_source_languages - category_manga_sync_accounts + category_tracking_accounts pref_clear_chapter_cache_key pref_clear_database_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3a856f25e..9655a1cf1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -80,7 +80,7 @@ Reader Downloads Sources - Sync + Tracking Advanced About @@ -276,13 +276,14 @@ Are you sure you want to delete selected chapters? + Tracking Reading Completed Dropped On hold Plan to read Score - Title… + Title Status Chapters diff --git a/app/src/main/res/xml/pref_sync.xml b/app/src/main/res/xml/pref_tracking.xml similarity index 62% rename from app/src/main/res/xml/pref_sync.xml rename to app/src/main/res/xml/pref_tracking.xml index 82caccdff..15f0d1787 100644 --- a/app/src/main/res/xml/pref_sync.xml +++ b/app/src/main/res/xml/pref_tracking.xml @@ -1,30 +1,32 @@ - + android:defaultValue="true" + app:showText="false"/> + android:dependency="@string/pref_auto_update_manga_sync_key" + app:showText="false"/> + android:persistent="false" + app:showText="false"/> diff --git a/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt b/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt index 37b358e86..70b29e252 100644 --- a/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt +++ b/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt @@ -390,16 +390,16 @@ class BackupTest { @Test fun testRestoreSyncForManga() { - // Create a manga and mangaSync + // Create a manga and track val manga = createManga("title") manga.id = 1L - val mangaSync = createMangaSync(manga, 1, 2, 3) + val track = createTrack(manga, 1, 2, 3) // Add an entry for the manga val entry = JsonObject() entry.add("manga", toJson(manga)) - entry.add("sync", toJson(mangaSync)) + entry.add("sync", toJson(track)) // Append the entry to the backup list val mangas = ArrayList() @@ -412,7 +412,7 @@ class BackupTest { val dbManga = db.getManga(1).executeAsBlocking() assertThat(dbManga).isNotNull() - val dbSync = db.getMangasSync(dbManga!!).executeAsBlocking() + val dbSync = db.getTracks(dbManga!!).executeAsBlocking() assertThat(dbSync).hasSize(3) } @@ -422,13 +422,13 @@ class BackupTest { // Create a manga and 3 sync val manga = createManga("title") manga.id = mangaId - val mangaSync = createMangaSync(manga, 1, 2, 3) + val track = createTrack(manga, 1, 2, 3) db.insertManga(manga).executeAsBlocking() // Add an entry for the manga val entry = JsonObject() entry.add("manga", toJson(manga)) - entry.add("sync", toJson(mangaSync)) + entry.add("sync", toJson(track)) // Append the entry to the backup list val mangas = ArrayList() @@ -441,7 +441,7 @@ class BackupTest { val dbManga = db.getManga(mangaId).executeAsBlocking() assertThat(dbManga).isNotNull() - val dbSync = db.getMangasSync(dbManga!!).executeAsBlocking() + val dbSync = db.getTracks(dbManga!!).executeAsBlocking() assertThat(dbSync).hasSize(3) } @@ -451,17 +451,17 @@ class BackupTest { // Store a manga and 3 sync val manga = createManga("title") manga.id = mangaId - var mangaSync = createMangaSync(manga, 1, 2, 3) + var track = createTrack(manga, 1, 2, 3) 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 - mangaSync = createMangaSync(manga, 3, 4) + track = createTrack(manga, 3, 4) // Add an entry for the manga val entry = JsonObject() entry.add("manga", toJson(manga)) - entry.add("sync", toJson(mangaSync)) + entry.add("sync", toJson(track)) // Append the entry to the backup list val mangas = ArrayList() @@ -474,7 +474,7 @@ class BackupTest { val dbManga = db.getManga(mangaId).executeAsBlocking() assertThat(dbManga).isNotNull() - val dbSync = db.getMangasSync(dbManga!!).executeAsBlocking() + val dbSync = db.getTracks(dbManga!!).executeAsBlocking() assertThat(dbSync).hasSize(4) } @@ -546,17 +546,17 @@ class BackupTest { return chapters } - private fun createMangaSync(manga: Manga, syncId: Int): MangaSync { - val m = MangaSync.create(syncId) + private fun createTrack(manga: Manga, syncId: Int): Track { + val m = Track.create(syncId) m.manga_id = manga.id!! m.title = "title" return m } - private fun createMangaSync(manga: Manga, vararg syncIds: Int): List { - val ms = ArrayList() + private fun createTrack(manga: Manga, vararg syncIds: Int): List { + val ms = ArrayList() for (title in syncIds) { - ms.add(createMangaSync(manga, title)) + ms.add(createTrack(manga, title)) } return ms }