Experimental Anilist and Kitsu support (#586)

* Tracking tab with anilist support

* Rename MangaSync to Track

* Rename variables and methods to track

* Kitsu implementation

* Variables refactoring

* Travis fix?
This commit is contained in:
inorichi 2016-12-18 22:56:28 +01:00 committed by GitHub
parent e3d430eb5e
commit 94ee4e7fb5
76 changed files with 2310 additions and 1654 deletions

View File

@ -12,11 +12,21 @@ android:
- extra-android-support
- extra-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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,7 +39,7 @@ class BackupManager(private val db: DatabaseHelper) {
private val MANGA = "manga"
private val MANGAS = "mangas"
private val CHAPTERS = "chapters"
private val MANGA_SYNC = "sync"
private val TRACK = "sync"
private val CATEGORIES = "categories"
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
@ -109,10 +109,10 @@ class BackupManager(private val db: DatabaseHelper) {
entry.add(CHAPTERS, gson.toJsonTree(chapters))
}
// Backup manga sync
val mangaSync = db.getMangasSync(manga).executeAsBlocking()
if (!mangaSync.isEmpty()) {
entry.add(MANGA_SYNC, gson.toJsonTree(mangaSync))
// Backup tracks
val tracks = db.getTracks(manga).executeAsBlocking()
if (!tracks.isEmpty()) {
entry.add(TRACK, gson.toJsonTree(tracks))
}
// Backup categories for this manga
@ -231,13 +231,13 @@ class BackupManager(private val db: DatabaseHelper) {
val element = backupManga.asJsonObject
val manga = gson.fromJson(element.get(MANGA), MangaImpl::class.java)
val chapters = gson.fromJson<List<ChapterImpl>>(element.get(CHAPTERS) ?: JsonArray())
val sync = gson.fromJson<List<MangaSyncImpl>>(element.get(MANGA_SYNC) ?: JsonArray())
val tracks = gson.fromJson<List<TrackImpl>>(element.get(TRACK) ?: JsonArray())
val categories = gson.fromJson<List<String>>(element.get(CATEGORIES) ?: JsonArray())
// 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<MangaSync>) {
private fun restoreSyncForManga(manga: Manga, tracks: List<Track>) {
// 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<MangaSync>()
for (backupSync in sync) {
val dbTracks = db.getTracks(manga).executeAsBlocking()
val trackToUpdate = ArrayList<Track>()
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()
}
}

View File

@ -5,7 +5,7 @@ import com.google.gson.FieldAttributes
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
import eu.kanade.tachiyomi.data.database.models.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
}

View File

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

View File

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

View File

@ -9,38 +9,38 @@ import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.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<MangaSync>(
MangaSyncPutResolver(),
MangaSyncGetResolver(),
MangaSyncDeleteResolver()
class TrackTypeMapping : SQLiteTypeMapping<Track>(
TrackPutResolver(),
TrackGetResolver(),
TrackDeleteResolver()
)
class MangaSyncPutResolver : DefaultPutResolver<MangaSync>() {
class TrackPutResolver : DefaultPutResolver<Track>() {
override fun mapToInsertQuery(obj: MangaSync) = InsertQuery.builder()
override fun mapToInsertQuery(obj: Track) = InsertQuery.builder()
.table(TABLE)
.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<MangaSync>() {
}
}
class MangaSyncGetResolver : DefaultGetResolver<MangaSync>() {
class TrackGetResolver : DefaultGetResolver<Track>() {
override fun mapFromCursor(cursor: Cursor): MangaSync = MangaSyncImpl().apply {
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
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<MangaSync>() {
}
}
class MangaSyncDeleteResolver : DefaultDeleteResolver<MangaSync>() {
class TrackDeleteResolver : DefaultDeleteResolver<Track>() {
override fun mapToDeleteQuery(obj: MangaSync) = DeleteQuery.builder()
override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)

View File

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

View File

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

View File

@ -1,46 +0,0 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
interface MangaSyncQueries : DbProvider {
fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get()
.`object`(MangaSync::class.java)
.withQuery(Query.builder()
.table(MangaSyncTable.TABLE)
.where("${MangaSyncTable.COL_MANGA_ID} = ? AND " +
"${MangaSyncTable.COL_SYNC_ID} = ?")
.whereArgs(manga.id, sync.id)
.build())
.prepare()
fun getMangasSync(manga: Manga) = db.get()
.listOfObjects(MangaSync::class.java)
.withQuery(Query.builder()
.table(MangaSyncTable.TABLE)
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
.whereArgs(manga.id)
.build())
.prepare()
fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare()
fun insertMangasSync(mangas: List<MangaSync>) = db.put().objects(mangas).prepare()
fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
fun deleteMangaSyncForManga(manga: Manga) = db.delete()
.byQuery(DeleteQuery.builder()
.table(MangaSyncTable.TABLE)
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
.whereArgs(manga.id)
.build())
.prepare()
}

View File

@ -0,0 +1,34 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.tables.TrackTable
import eu.kanade.tachiyomi.data.track.TrackService
interface TrackQueries : DbProvider {
fun getTracks(manga: Manga) = db.get()
.listOfObjects(Track::class.java)
.withQuery(Query.builder()
.table(TrackTable.TABLE)
.where("${TrackTable.COL_MANGA_ID} = ?")
.whereArgs(manga.id)
.build())
.prepare()
fun insertTrack(track: Track) = db.put().`object`(track).prepare()
fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare()
fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete()
.byQuery(DeleteQuery.builder()
.table(TrackTable.TABLE)
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
.whereArgs(manga.id, sync.id)
.build())
.prepare()
}

View File

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

View File

@ -1,23 +0,0 @@
package eu.kanade.tachiyomi.data.mangasync
import android.content.Context
import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist
import eu.kanade.tachiyomi.data.mangasync.myanimelist.MyAnimeList
class MangaSyncManager(private val context: Context) {
companion object {
const val MYANIMELIST = 1
const val ANILIST = 2
}
val myAnimeList = MyAnimeList(context, MYANIMELIST)
val aniList = Anilist(context, ANILIST)
// TODO enable anilist
val services = listOf(myAnimeList)
fun getService(id: Int) = services.find { it.id == id }
}

View File

@ -1,51 +0,0 @@
package eu.kanade.tachiyomi.data.mangasync
import android.content.Context
import android.support.annotation.CallSuper
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import okhttp3.OkHttpClient
import rx.Completable
import rx.Observable
import uy.kohesive.injekt.injectLazy
abstract class MangaSyncService(private val context: Context, val id: Int) {
val preferences: PreferencesHelper by injectLazy()
val networkService: NetworkHelper by injectLazy()
open val client: OkHttpClient
get() = networkService.client
// Name of the manga sync service to display
abstract val name: String
abstract fun login(username: String, password: String): Completable
open val isLogged: Boolean
get() = !getUsername().isEmpty() &&
!getPassword().isEmpty()
abstract fun add(manga: MangaSync): Observable<MangaSync>
abstract fun update(manga: MangaSync): Observable<MangaSync>
abstract fun bind(manga: MangaSync): Observable<MangaSync>
abstract fun getStatus(status: Int): String
fun saveCredentials(username: String, password: String) {
preferences.setMangaSyncCredentials(this, username, password)
}
@CallSuper
open fun logout() {
preferences.setMangaSyncCredentials(this, "", "")
}
fun getUsername() = preferences.mangaSyncUsername(this)
fun getPassword() = preferences.mangaSyncPassword(this)
}

View File

@ -1,132 +0,0 @@
package eu.kanade.tachiyomi.data.mangasync.anilist
import android.content.Context
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
import rx.Completable
import rx.Observable
import timber.log.Timber
class Anilist(private val context: Context, id: Int) : MangaSyncService(context, id) {
companion object {
const val READING = 1
const val COMPLETED = 2
const val ON_HOLD = 3
const val DROPPED = 4
const val PLAN_TO_READ = 5
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
}
override val name = "AniList"
private val interceptor by lazy { AnilistInterceptor(getPassword()) }
private val api by lazy {
AnilistApi.createService(networkService.client.newBuilder()
.addInterceptor(interceptor)
.build())
}
override fun login(username: String, password: String) = login(password)
fun login(authCode: String): Completable {
// Create a new api with the default client to avoid request interceptions.
return AnilistApi.createService(client)
// Request the access token from the API with the authorization code.
.requestAccessToken(authCode)
// Save the token in the interceptor.
.doOnNext { interceptor.setAuth(it) }
// Obtain the authenticated user from the API.
.zipWith(api.getCurrentUser().map { it["id"].toString() })
{ oauth, user -> Pair(user, oauth.refresh_token!!) }
// Save service credentials (username and refresh token).
.doOnNext { saveCredentials(it.first, it.second) }
// Logout on any error.
.doOnError { logout() }
.toCompletable()
}
override fun logout() {
super.logout()
interceptor.setAuth(null)
}
fun search(query: String): Observable<List<MangaSync>> {
return api.search(query, 1)
.flatMap { Observable.from(it) }
.filter { it.type != "Novel" }
.map { it.toMangaSync() }
.toList()
}
fun getList(): Observable<List<MangaSync>> {
return api.getList(getUsername())
.flatMap { Observable.from(it.flatten()) }
.map { it.toMangaSync() }
.toList()
}
override fun add(manga: MangaSync): Observable<MangaSync> {
return api.addManga(manga.remote_id, manga.last_chapter_read, manga.getAnilistStatus(),
manga.score.toInt())
.doOnNext { it.body().close() }
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
.doOnError { Timber.e(it, it.message) }
.map { manga }
}
override fun update(manga: MangaSync): Observable<MangaSync> {
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
manga.status = COMPLETED
}
return api.updateManga(manga.remote_id, manga.last_chapter_read, manga.getAnilistStatus(),
manga.score.toInt())
.doOnNext { it.body().close() }
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
.doOnError { Timber.e(it, it.message) }
.map { manga }
}
override fun bind(manga: MangaSync): Observable<MangaSync> {
return getList()
.flatMap { userlist ->
manga.sync_id = id
val mangaFromList = userlist.find { it.remote_id == manga.remote_id }
if (mangaFromList != null) {
manga.copyPersonalFrom(mangaFromList)
update(manga)
} else {
// Set default fields if it's not found in the list
manga.score = DEFAULT_SCORE.toFloat()
manga.status = DEFAULT_STATUS
add(manga)
}
}
}
override fun getStatus(status: Int): String = with(context) {
when (status) {
READING -> getString(R.string.reading)
COMPLETED -> getString(R.string.completed)
ON_HOLD -> getString(R.string.on_hold)
DROPPED -> getString(R.string.dropped)
PLAN_TO_READ -> getString(R.string.plan_to_read)
else -> ""
}
}
private fun MangaSync.getAnilistStatus() = when (status) {
READING -> "reading"
COMPLETED -> "completed"
ON_HOLD -> "on-hold"
DROPPED -> "dropped"
PLAN_TO_READ -> "plan to read"
else -> throw NotImplementedError("Unknown status")
}
}

View File

@ -51,9 +51,9 @@ class PreferenceKeys(context: Context) {
val updateOnlyNonCompleted = context.getString(R.string.pref_update_only_non_completed_key)
val 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)

View File

@ -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 <T> Preference<T>.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)

View File

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

View File

@ -0,0 +1,28 @@
package eu.kanade.tachiyomi.data.track
import android.content.Context
import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
class TrackManager(private val context: Context) {
companion object {
const val MYANIMELIST = 1
const val ANILIST = 2
const val KITSU = 3
}
val myAnimeList = MyAnimeList(context, MYANIMELIST)
val aniList = Anilist(context, ANILIST)
val kitsu = Kitsu(context, KITSU)
val services = listOf(myAnimeList, aniList, kitsu)
fun getService(id: Int) = services.find { it.id == id }
fun hasLoggedServices() = services.any { it.isLogged }
}

View File

@ -0,0 +1,67 @@
package eu.kanade.tachiyomi.data.track
import android.support.annotation.CallSuper
import android.support.annotation.DrawableRes
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import okhttp3.OkHttpClient
import rx.Completable
import rx.Observable
import uy.kohesive.injekt.injectLazy
abstract class TrackService(val id: Int) {
val preferences: PreferencesHelper by injectLazy()
val networkService: NetworkHelper by injectLazy()
open val client: OkHttpClient
get() = networkService.client
// Name of the manga sync service to display
abstract val name: String
abstract fun login(username: String, password: String): Completable
open val isLogged: Boolean
get() = !getUsername().isEmpty() &&
!getPassword().isEmpty()
abstract fun add(track: Track): Observable<Track>
abstract fun update(track: Track): Observable<Track>
abstract fun bind(track: Track): Observable<Track>
abstract fun search(query: String): Observable<List<Track>>
abstract fun refresh(track: Track): Observable<Track>
abstract fun getStatus(status: Int): String
abstract fun getStatusList(): List<Int>
@DrawableRes
abstract fun getLogo(): Int
abstract fun getLogoColor(): Int
// TODO better support (decimals)
abstract fun maxScore(): Int
abstract fun formatScore(track: Track): String
fun saveCredentials(username: String, password: String) {
preferences.setTrackCredentials(this, username, password)
}
@CallSuper
open fun logout() {
preferences.setTrackCredentials(this, "", "")
}
fun getUsername() = preferences.trackUsername(this)
fun getPassword() = preferences.trackPassword(this)
}

View File

@ -1,20 +1,20 @@
package eu.kanade.tachiyomi.data.mangasync
package eu.kanade.tachiyomi.data.track
import android.app.Service
import android.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)
}
}

View File

@ -0,0 +1,191 @@
package eu.kanade.tachiyomi.data.track.anilist
import android.content.Context
import android.graphics.Color
import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.string
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackService
import rx.Completable
import rx.Observable
import timber.log.Timber
class Anilist(private val context: Context, id: Int) : TrackService(id) {
companion object {
const val READING = 1
const val COMPLETED = 2
const val ON_HOLD = 3
const val DROPPED = 4
const val PLAN_TO_READ = 5
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
}
override val name = "AniList"
private val interceptor by lazy { AnilistInterceptor(getPassword()) }
private val api by lazy {
AnilistApi.createService(networkService.client.newBuilder()
.addInterceptor(interceptor)
.build())
}
override fun getLogo() = R.drawable.al
override fun getLogoColor() = Color.rgb(18, 25, 35)
override fun maxScore() = 100
override fun login(username: String, password: String) = login(password)
fun login(authCode: String): Completable {
// Create a new api with the default client to avoid request interceptions.
return AnilistApi.createService(client)
// Request the access token from the API with the authorization code.
.requestAccessToken(authCode)
// Save the token in the interceptor.
.doOnNext { interceptor.setAuth(it) }
// Obtain the authenticated user from the API.
.zipWith(api.getCurrentUser().map {
preferences.anilistScoreType().set(it["score_type"].int)
it["id"].string
}, { oauth, user -> Pair(user, oauth.refresh_token!!) })
// Save service credentials (username and refresh token).
.doOnNext { saveCredentials(it.first, it.second) }
// Logout on any error.
.doOnError { logout() }
.toCompletable()
}
override fun logout() {
super.logout()
interceptor.setAuth(null)
}
override fun search(query: String): Observable<List<Track>> {
return api.search(query, 1)
.flatMap { Observable.from(it) }
.filter { it.type != "Novel" }
.map { it.toTrack() }
.toList()
}
fun getList(): Observable<List<Track>> {
return api.getList(getUsername())
.flatMap { Observable.from(it.flatten()) }
.map { it.toTrack() }
.toList()
}
override fun add(track: Track): Observable<Track> {
return api.addManga(track.remote_id, track.last_chapter_read, track.getAnilistStatus())
.doOnNext { it.body().close() }
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
.doOnError { Timber.e(it) }
.map { track }
}
override fun update(track: Track): Observable<Track> {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = COMPLETED
}
return api.updateManga(track.remote_id, track.last_chapter_read, track.getAnilistStatus(),
track.getAnilistScore())
.doOnNext { it.body().close() }
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
.doOnError { Timber.e(it) }
.map { track }
}
override fun bind(track: Track): Observable<Track> {
return getList()
.flatMap { userlist ->
track.sync_id = id
val remoteTrack = userlist.find { it.remote_id == track.remote_id }
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
update(track)
} else {
// Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS
add(track)
}
}
}
override fun refresh(track: Track): Observable<Track> {
return getList()
.map { myList ->
val remoteTrack = myList.find { it.remote_id == track.remote_id }
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
track
} else {
throw Exception("Could not find manga")
}
}
}
override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
}
override fun getStatus(status: Int): String = with(context) {
when (status) {
READING -> getString(R.string.reading)
COMPLETED -> getString(R.string.completed)
ON_HOLD -> getString(R.string.on_hold)
DROPPED -> getString(R.string.dropped)
PLAN_TO_READ -> getString(R.string.plan_to_read)
else -> ""
}
}
private fun Track.getAnilistStatus() = when (status) {
READING -> "reading"
COMPLETED -> "completed"
ON_HOLD -> "on-hold"
DROPPED -> "dropped"
PLAN_TO_READ -> "plan to read"
else -> throw NotImplementedError("Unknown status")
}
fun Track.getAnilistScore(): String = when (preferences.anilistScoreType().getOrDefault()) {
// 10 point
0 -> Math.floor(score.toDouble() / 10).toInt().toString()
// 100 point
1 -> score.toInt().toString()
// 5 stars
2 -> when {
score == 0f -> "0"
score < 30 -> "1"
score < 50 -> "2"
score < 70 -> "3"
score < 90 -> "4"
else -> "5"
}
// Smiley
3 -> when {
score == 0f -> "0"
score <= 30 -> ":("
score <= 60 -> ":|"
else -> ":)"
}
// 10 point decimal
4 -> (score / 10).toString()
else -> throw Exception("Unknown score type")
}
override fun formatScore(track: Track): String {
return track.getAnilistScore()
}
}

View File

@ -1,11 +1,11 @@
package eu.kanade.tachiyomi.data.mangasync.anilist
package eu.kanade.tachiyomi.data.track.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 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
@ -73,8 +73,7 @@ interface AnilistApi {
fun addManga(
@Field("id") id: Int,
@Field("chapters_read") chapters_read: Int,
@Field("list_status") list_status: String,
@Field("score_raw") score_raw: Int)
@Field("list_status") list_status: String)
: Observable<Response<ResponseBody>>
@FormUrlEncoded
@ -83,7 +82,7 @@ interface AnilistApi {
@Field("id") id: Int,
@Field("chapters_read") chapters_read: Int,
@Field("list_status") list_status: String,
@Field("score_raw") score_raw: Int)
@Field("score") score_raw: String)
: Observable<Response<ResponseBody>>
}

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.mangasync.anilist
package eu.kanade.tachiyomi.data.track.anilist
import com.google.gson.Gson
import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth
import eu.kanade.tachiyomi.data.track.anilist.model.OAuth
import okhttp3.Interceptor
import okhttp3.Response

View File

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

View File

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

View File

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.data.mangasync.anilist.model
package eu.kanade.tachiyomi.data.track.anilist.model
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist
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,
@ -11,14 +11,14 @@ data class ALUserManga(
val chapters_read: Int,
val manga: ALManga) {
fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply {
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
remote_id = manga.id
status = getMangaSyncStatus()
status = toTrackStatus()
score = score_raw.toFloat()
last_chapter_read = chapters_read
}
fun getMangaSyncStatus() = when (list_status) {
fun toTrackStatus() = when (list_status) {
"reading" -> Anilist.READING
"completed" -> Anilist.COMPLETED
"on-hold" -> Anilist.ON_HOLD

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.data.mangasync.anilist.model
package eu.kanade.tachiyomi.data.track.anilist.model
data class OAuth(
val access_token: String,

View File

@ -0,0 +1,219 @@
package eu.kanade.tachiyomi.data.track.kitsu
import android.content.Context
import android.graphics.Color
import com.github.salomonbrys.kotson.*
import com.google.gson.Gson
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackService
import rx.Completable
import rx.Observable
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
class Kitsu(private val context: Context, id: Int) : TrackService(id) {
companion object {
const val READING = 1
const val COMPLETED = 2
const val ON_HOLD = 3
const val DROPPED = 4
const val PLAN_TO_READ = 5
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0f
}
override val name = "Kitsu"
private val gson: Gson by injectLazy()
private val interceptor by lazy { KitsuInterceptor(this, gson) }
private val api by lazy {
KitsuApi.createService(client.newBuilder()
.addInterceptor(interceptor)
.build())
}
private fun getUserId(): String {
return getPassword()
}
fun saveToken(oauth: OAuth?) {
val json = gson.toJson(oauth)
preferences.trackToken(this).set(json)
}
fun restoreToken(): OAuth? {
return try {
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
} catch (e: Exception) {
null
}
}
override fun login(username: String, password: String): Completable {
return KitsuApi.createLoginService(client)
.requestAccessToken(username, password)
.doOnNext { interceptor.newAuth(it) }
.flatMap { api.getCurrentUser().map { it["data"].array[0]["id"].string } }
.doOnNext { userId -> saveCredentials(username, userId) }
.doOnError { logout() }
.toCompletable()
}
override fun logout() {
super.logout()
interceptor.newAuth(null)
}
override fun search(query: String): Observable<List<Track>> {
return api.search(query)
.map { json ->
val data = json["data"].array
data.map { KitsuManga(it.obj).toTrack() }
}
.doOnError { Timber.e(it) }
}
override fun bind(track: Track): Observable<Track> {
return find(track)
.flatMap { remoteTrack ->
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.remote_id = remoteTrack.remote_id
update(track)
} else {
track.score = DEFAULT_SCORE
track.status = DEFAULT_STATUS
add(track)
}
}
}
private fun find(track: Track): Observable<Track?> {
return api.findLibManga(getUserId(), track.remote_id)
.map { json ->
val data = json["data"].array
if (data.size() > 0) {
KitsuLibManga(data[0].obj, json["included"].array[0].obj).toTrack()
} else {
null
}
}
}
override fun add(track: Track): Observable<Track> {
// @formatter:off
val data = jsonObject(
"type" to "libraryEntries",
"attributes" to jsonObject(
"status" to track.getKitsuStatus(),
"progress" to track.last_chapter_read
),
"relationships" to jsonObject(
"user" to jsonObject(
"data" to jsonObject(
"id" to getUserId(),
"type" to "users"
)
),
"media" to jsonObject(
"data" to jsonObject(
"id" to track.remote_id,
"type" to "manga"
)
)
)
)
// @formatter:on
return api.addLibManga(jsonObject("data" to data))
.doOnNext { json -> track.remote_id = json["data"]["id"].int }
.doOnError { Timber.e(it) }
.map { track }
}
override fun update(track: Track): Observable<Track> {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = COMPLETED
}
// @formatter:off
val data = jsonObject(
"type" to "libraryEntries",
"id" to track.remote_id,
"attributes" to jsonObject(
"status" to track.getKitsuStatus(),
"progress" to track.last_chapter_read,
"rating" to track.getKitsuScore()
)
)
// @formatter:on
return api.updateLibManga(track.remote_id, jsonObject("data" to data))
.map { track }
}
override fun refresh(track: Track): Observable<Track> {
return api.getLibManga(track.remote_id)
.map { json ->
val data = json["data"].array
if (data.size() > 0) {
val include = json["included"].array[0].obj
val remoteTrack = KitsuLibManga(data[0].obj, include).toTrack()
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
track
} else {
throw Exception("Could not find manga")
}
}
}
override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
}
override fun getStatus(status: Int): String = with(context) {
when (status) {
READING -> getString(R.string.reading)
COMPLETED -> getString(R.string.completed)
ON_HOLD -> getString(R.string.on_hold)
DROPPED -> getString(R.string.dropped)
PLAN_TO_READ -> getString(R.string.plan_to_read)
else -> ""
}
}
private fun Track.getKitsuStatus() = when (status) {
READING -> "current"
COMPLETED -> "completed"
ON_HOLD -> "on_hold"
DROPPED -> "dropped"
PLAN_TO_READ -> "planned"
else -> throw Exception("Unknown status")
}
private fun Track.getKitsuScore(): String {
return if (score > 0) (score / 2).toString() else ""
}
override fun getLogo(): Int {
return R.drawable.kitsu
}
override fun getLogoColor(): Int {
return Color.rgb(51, 37, 50)
}
override fun maxScore(): Int {
return 10
}
override fun formatScore(track: Track): String {
return track.getKitsuScore()
}
}

View File

@ -0,0 +1,93 @@
package eu.kanade.tachiyomi.data.track.kitsu
import com.google.gson.JsonObject
import eu.kanade.tachiyomi.data.network.POST
import okhttp3.FormBody
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.*
import rx.Observable
interface KitsuApi {
companion object {
private const val clientId = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
private const val clientSecret = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
private const val baseUrl = "https://kitsu.io/api/edge/"
private const val loginUrl = "https://kitsu.io/api/"
fun createService(client: OkHttpClient) = Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build()
.create(KitsuApi::class.java)
fun createLoginService(client: OkHttpClient) = Retrofit.Builder()
.baseUrl(loginUrl)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build()
.create(KitsuApi::class.java)
fun refreshTokenRequest(token: String) = POST("${loginUrl}oauth/token",
body = FormBody.Builder()
.add("grant_type", "refresh_token")
.add("client_id", clientId)
.add("client_secret", clientSecret)
.add("refresh_token", token)
.build())
}
@FormUrlEncoded
@POST("oauth/token")
fun requestAccessToken(
@Field("username") username: String,
@Field("password") password: String,
@Field("grant_type") grantType: String = "password",
@Field("client_id") client_id: String = clientId,
@Field("client_secret") client_secret: String = clientSecret
) : Observable<OAuth>
@GET("users")
fun getCurrentUser(
@Query("filter[self]", encoded = true) self: Boolean = true
) : Observable<JsonObject>
@GET("manga")
fun search(
@Query("filter[text]", encoded = true) query: String
): Observable<JsonObject>
@GET("library-entries")
fun getLibManga(
@Query("filter[id]", encoded = true) remoteId: Int,
@Query("include") includes: String = "media"
) : Observable<JsonObject>
@GET("library-entries")
fun findLibManga(
@Query("filter[user_id]", encoded = true) userId: String,
@Query("filter[media_id]", encoded = true) remoteId: Int,
@Query("page[limit]", encoded = true) limit: Int = 10000,
@Query("include") includes: String = "media"
) : Observable<JsonObject>
@Headers("Content-Type: application/vnd.api+json")
@POST("library-entries")
fun addLibManga(
@Body data: JsonObject
) : Observable<JsonObject>
@Headers("Content-Type: application/vnd.api+json")
@PATCH("library-entries/{id}")
fun updateLibManga(
@Path("id") remoteId: Int,
@Body data: JsonObject
) : Observable<JsonObject>
}

View File

@ -0,0 +1,46 @@
package eu.kanade.tachiyomi.data.track.kitsu
import com.google.gson.Gson
import okhttp3.Interceptor
import okhttp3.Response
class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor {
/**
* OAuth object used for authenticated requests.
*/
private var oauth: OAuth? = kitsu.restoreToken()
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val currAuth = oauth ?: throw Exception("Not authenticated with Kitsu")
val refreshToken = currAuth.refresh_token!!
// Refresh access token if expired.
if (currAuth.isExpired()) {
val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken))
if (response.isSuccessful) {
newAuth(gson.fromJson(response.body().string(), OAuth::class.java))
} else {
response.close()
}
}
// Add the authorization header to the original request.
val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.header("Accept", "application/vnd.api+json")
.header("Content-Type", "application/vnd.api+json")
.build()
return chain.proceed(authRequest)
}
fun newAuth(oauth: OAuth?) {
this.oauth = oauth
kitsu.saveToken(oauth)
}
}

View File

@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.data.track.kitsu
import android.support.annotation.CallSuper
import com.github.salomonbrys.kotson.*
import com.google.gson.JsonObject
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager
open class KitsuManga(obj: JsonObject) {
val id by obj.byInt
val canonicalTitle by obj["attributes"].byString
val chapterCount = obj["attributes"]["chapterCount"].nullInt
@CallSuper
open fun toTrack() = Track.create(TrackManager.KITSU).apply {
remote_id = this@KitsuManga.id
title = canonicalTitle
total_chapters = chapterCount ?: 0
}
}
class KitsuLibManga(obj: JsonObject, manga: JsonObject) : KitsuManga(manga) {
val remoteId by obj.byInt("id")
val status by obj["attributes"].byString
val rating = obj["attributes"]["rating"].nullString
val progress by obj["attributes"].byInt
override fun toTrack() = super.toTrack().apply {
remote_id = remoteId
status = toTrackStatus()
score = rating?.let { it.toFloat() * 2 } ?: 0f
last_chapter_read = progress
}
private fun toTrackStatus() = when (status) {
"current" -> Kitsu.READING
"completed" -> Kitsu.COMPLETED
"on_hold" -> Kitsu.ON_HOLD
"dropped" -> Kitsu.DROPPED
"planned" -> Kitsu.PLAN_TO_READ
else -> throw Exception("Unknown status")
}
}

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.data.track.kitsu
data class OAuth(
val access_token: String,
val token_type: String,
val created_at: Long,
val expires_in: Long,
val refresh_token: String?) {
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
}

View File

@ -1,14 +1,15 @@
package eu.kanade.tachiyomi.data.mangasync.myanimelist
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.MangaSync
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.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
@ -21,26 +22,28 @@ import rx.Completable
import rx.Observable
import java.io.StringWriter
class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) {
class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
private lateinit var headers: Headers
companion object {
val BASE_URL = "https://myanimelist.net"
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"
val READING = 1
val COMPLETED = 2
val ON_HOLD = 3
val DROPPED = 4
val PLAN_TO_READ = 6
const val READING = 1
const val COMPLETED = 2
const val ON_HOLD = 3
const val DROPPED = 4
const val PLAN_TO_READ = 6
val DEFAULT_STATUS = READING
val DEFAULT_SCORE = 0
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
const val PREFIX_MY = "my:"
}
init {
@ -55,6 +58,16 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
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()
@ -71,14 +84,14 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
.appendQueryParameter("type", "manga")
.toString()
fun getUpdateUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
fun getUpdateUrl(track: Track) = Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/update")
.appendPath("${manga.remote_id}.xml")
.appendPath("${track.remote_id}.xml")
.toString()
fun getAddUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
fun getAddUrl(track: Track) = Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/add")
.appendPath("${manga.remote_id}.xml")
.appendPath("${track.remote_id}.xml")
.toString()
override fun login(username: String, password: String): Completable {
@ -87,34 +100,58 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
.asObservable()
.doOnNext { it.close() }
.doOnNext { if (it.code() != 200) throw Exception("Login error") }
.doOnNext { saveCredentials(username, password) }
.doOnError { logout() }
.toCompletable()
}
fun search(query: String): Observable<List<MangaSync>> {
return client.newCall(GET(getSearchUrl(query), headers))
.asObservable()
.map { Jsoup.parse(it.body().string()) }
.flatMap { Observable.from(it.select("entry")) }
.filter { it.select("type").text() != "Novel" }
.map {
MangaSync.create(id).apply {
title = it.selectText("title")!!
remote_id = it.selectInt("id")
total_chapters = it.selectInt("chapters")
override fun search(query: String): Observable<List<Track>> {
return if (query.startsWith(PREFIX_MY)) {
val realQuery = query.substring(PREFIX_MY.length).toLowerCase().trim()
getList()
.flatMap { Observable.from(it) }
.filter { realQuery in it.title.toLowerCase() }
.toList()
} else {
client.newCall(GET(getSearchUrl(query), headers))
.asObservable()
.map { Jsoup.parse(it.body().string()) }
.flatMap { Observable.from(it.select("entry")) }
.filter { it.select("type").text() != "Novel" }
.map {
Track.create(id).apply {
title = it.selectText("title")!!
remote_id = it.selectInt("id")
total_chapters = it.selectInt("chapters")
}
}
.toList()
}
}
override fun refresh(track: Track): Observable<Track> {
return getList()
.map { myList ->
val remoteTrack = myList.find { it.remote_id == track.remote_id }
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
track
} else {
throw Exception("Could not find manga")
}
}
.toList()
}
// MAL doesn't support score with decimals
fun getList(): Observable<List<MangaSync>> {
fun getList(): Observable<List<Track>> {
return networkService.forceCacheClient
.newCall(GET(getListUrl(getUsername()), headers))
.asObservable()
.map { Jsoup.parse(it.body().string()) }
.flatMap { Observable.from(it.select("manga")) }
.map {
MangaSync.create(id).apply {
Track.create(id).apply {
title = it.selectText("series_title")!!
remote_id = it.selectInt("series_mangadb_id")
last_chapter_read = it.selectInt("my_read_chapters")
@ -126,31 +163,31 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
.toList()
}
override fun update(manga: MangaSync): Observable<MangaSync> {
override fun update(track: Track): Observable<Track> {
return Observable.defer {
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
manga.status = COMPLETED
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = COMPLETED
}
client.newCall(POST(getUpdateUrl(manga), headers, getMangaPostPayload(manga)))
client.newCall(POST(getUpdateUrl(track), headers, getMangaPostPayload(track)))
.asObservable()
.doOnNext { it.close() }
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
.map { manga }
.map { track }
}
}
override fun add(manga: MangaSync): Observable<MangaSync> {
override fun add(track: Track): Observable<Track> {
return Observable.defer {
client.newCall(POST(getAddUrl(manga), headers, getMangaPostPayload(manga)))
client.newCall(POST(getAddUrl(track), headers, getMangaPostPayload(track)))
.asObservable()
.doOnNext { it.close() }
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
.map { manga }
.map { track }
}
}
private fun getMangaPostPayload(manga: MangaSync): RequestBody {
private fun getMangaPostPayload(track: Track): RequestBody {
val xml = Xml.newSerializer()
val writer = StringWriter()
@ -160,14 +197,14 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
startTag("", ENTRY_TAG)
// Last chapter read
if (manga.last_chapter_read != 0) {
inTag(CHAPTER_TAG, manga.last_chapter_read.toString())
if (track.last_chapter_read != 0) {
inTag(CHAPTER_TAG, track.last_chapter_read.toString())
}
// Manga status in the list
inTag(STATUS_TAG, manga.status.toString())
inTag(STATUS_TAG, track.status.toString())
// Manga score
inTag(SCORE_TAG, manga.score.toString())
inTag(SCORE_TAG, track.score.toString())
endTag("", ENTRY_TAG)
endDocument()
@ -184,19 +221,19 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
endTag(namespace, tag)
}
override fun bind(manga: MangaSync): Observable<MangaSync> {
override fun bind(track: Track): Observable<Track> {
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)
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
manga.score = DEFAULT_SCORE.toFloat()
manga.status = DEFAULT_STATUS
add(manga)
track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS
add(track)
}
}
}
@ -212,6 +249,10 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
}
}
override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
}
fun createHeaders(username: String, password: String) {
val builder = Headers.Builder()
builder.add("Authorization", Credentials.basic(username, password))

View File

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

View File

@ -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<MangaActivity>() {
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.

View File

@ -1,124 +0,0 @@
package eu.kanade.tachiyomi.ui.manga.myanimelist
import android.app.Dialog
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.view.View
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
import kotlinx.android.synthetic.main.dialog_myanimelist_search.view.*
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.subjects.PublishSubject
import java.util.concurrent.TimeUnit
class MyAnimeListDialogFragment : DialogFragment() {
companion object {
fun newInstance(): MyAnimeListDialogFragment {
return MyAnimeListDialogFragment()
}
}
private lateinit var v: View
lateinit var adapter: MyAnimeListSearchAdapter
private set
lateinit var querySubject: PublishSubject<String>
private set
private var selectedItem: MangaSync? = null
private var searchSubscription: Subscription? = null
override fun onCreateDialog(savedState: Bundle?): Dialog {
val dialog = MaterialDialog.Builder(activity)
.customView(R.layout.dialog_myanimelist_search, false)
.positiveText(android.R.string.ok)
.negativeText(android.R.string.cancel)
.onPositive { dialog1, which -> onPositiveButtonClick() }
.build()
onViewCreated(dialog.view, savedState)
return dialog
}
override fun onViewCreated(view: View, savedState: Bundle?) {
v = view
// Create adapter
adapter = MyAnimeListSearchAdapter(activity)
view.myanimelist_search_results.adapter = adapter
// Set listeners
view.myanimelist_search_results.setOnItemClickListener { parent, viewList, position, id ->
selectedItem = adapter.getItem(position)
}
// Do an initial search based on the manga's title
if (savedState == null) {
val title = presenter.manga.title
view.myanimelist_search_field.append(title)
search(title)
}
querySubject = PublishSubject.create<String>()
view.myanimelist_search_field.addTextChangedListener(object : SimpleTextWatcher() {
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
querySubject.onNext(s.toString())
}
})
}
override fun onResume() {
super.onResume()
// Listen to text changes
searchSubscription = querySubject.debounce(1, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { search(it) }
}
override fun onPause() {
searchSubscription?.unsubscribe()
super.onPause()
}
private fun onPositiveButtonClick() {
presenter.registerManga(selectedItem)
}
private fun search(query: String) {
if (!query.isNullOrEmpty()) {
v.myanimelist_search_results.visibility = View.GONE
v.progress.visibility = View.VISIBLE
presenter.searchManga(query)
}
}
fun onSearchResults(results: List<MangaSync>) {
selectedItem = null
v.progress.visibility = View.GONE
v.myanimelist_search_results.visibility = View.VISIBLE
adapter.setItems(results)
}
fun onSearchResultsError() {
v.progress.visibility = View.GONE
v.myanimelist_search_results.visibility = View.VISIBLE
adapter.clear()
}
val malFragment: MyAnimeListFragment
get() = parentFragment as MyAnimeListFragment
val presenter: MyAnimeListPresenter
get() = malFragment.presenter
}

View File

@ -1,177 +0,0 @@
package eu.kanade.tachiyomi.ui.manga.myanimelist
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.NumberPicker
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.card_myanimelist_personal.*
import kotlinx.android.synthetic.main.fragment_myanimelist.*
import nucleus.factory.RequiresPresenter
import java.text.DecimalFormat
@RequiresPresenter(MyAnimeListPresenter::class)
class MyAnimeListFragment : BaseRxFragment<MyAnimeListPresenter>() {
companion object {
fun newInstance(): MyAnimeListFragment {
return MyAnimeListFragment()
}
}
private var dialog: MyAnimeListDialogFragment? = null
private val decimalFormat = DecimalFormat("#.##")
private val SEARCH_FRAGMENT_TAG = "mal_search"
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_myanimelist, container, false)
}
override fun onViewCreated(view: View, savedState: Bundle?) {
swipe_refresh.isEnabled = false
swipe_refresh.setOnRefreshListener { presenter.refresh() }
myanimelist_title_layout.setOnClickListener { onTitleClick() }
myanimelist_status_layout.setOnClickListener { onStatusClick() }
myanimelist_chapters_layout.setOnClickListener { onChaptersClick() }
myanimelist_score_layout.setOnClickListener { onScoreClick() }
}
@Suppress("DEPRECATION")
fun setMangaSync(mangaSync: MangaSync?) {
swipe_refresh.isEnabled = mangaSync != null
mangaSync?.let {
myanimelist_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary)
myanimelist_title.setAllCaps(false)
myanimelist_title.text = it.title
myanimelist_chapters.text = if (it.total_chapters > 0)
"${it.last_chapter_read}/${it.total_chapters}" else "${it.last_chapter_read}/-"
myanimelist_score.text = if (it.score == 0f) "-" else decimalFormat.format(it.score)
myanimelist_status.text = presenter.myAnimeList.getStatus(it.status)
} ?: run {
myanimelist_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button)
myanimelist_title.setText(R.string.action_edit)
myanimelist_chapters.text = ""
myanimelist_score.text = ""
myanimelist_status.text = ""
}
}
fun onRefreshDone() {
swipe_refresh.isRefreshing = false
}
fun onRefreshError(error: Throwable) {
swipe_refresh.isRefreshing = false
context.toast(error.message)
}
fun setSearchResults(results: List<MangaSync>) {
findSearchFragmentIfNeeded()
dialog?.onSearchResults(results)
}
fun setSearchResultsError(error: Throwable) {
findSearchFragmentIfNeeded()
context.toast(error.message)
dialog?.onSearchResultsError()
}
private fun findSearchFragmentIfNeeded() {
if (dialog == null) {
dialog = childFragmentManager.findFragmentByTag(SEARCH_FRAGMENT_TAG) as MyAnimeListDialogFragment
}
}
fun onTitleClick() {
if (dialog == null) {
dialog = MyAnimeListDialogFragment.newInstance()
}
presenter.restartSearch()
dialog?.show(childFragmentManager, SEARCH_FRAGMENT_TAG)
}
fun onStatusClick() {
if (presenter.mangaSync == null)
return
MaterialDialog.Builder(activity)
.title(R.string.status)
.items(presenter.getAllStatus())
.itemsCallbackSingleChoice(presenter.getIndexFromStatus(), { dialog, view, i, charSequence ->
presenter.setStatus(i)
myanimelist_status.text = "..."
true
})
.show()
}
fun onChaptersClick() {
if (presenter.mangaSync == null)
return
val dialog = MaterialDialog.Builder(activity)
.title(R.string.chapters)
.customView(R.layout.dialog_myanimelist_chapters, false)
.positiveText(android.R.string.ok)
.negativeText(android.R.string.cancel)
.onPositive { d, action ->
val view = d.customView
if (view != null) {
val np = view.findViewById(R.id.chapters_picker) as NumberPicker
np.clearFocus()
presenter.setLastChapterRead(np.value)
myanimelist_chapters.text = "..."
}
}
.show()
val view = dialog.customView
if (view != null) {
val np = view.findViewById(R.id.chapters_picker) as NumberPicker
// Set initial value
np.value = presenter.mangaSync!!.last_chapter_read
// Don't allow to go from 0 to 9999
np.wrapSelectorWheel = false
}
}
fun onScoreClick() {
if (presenter.mangaSync == null)
return
val dialog = MaterialDialog.Builder(activity)
.title(R.string.score)
.customView(R.layout.dialog_myanimelist_score, false)
.positiveText(android.R.string.ok)
.negativeText(android.R.string.cancel)
.onPositive { d, action ->
val view = d.customView
if (view != null) {
val np = view.findViewById(R.id.score_picker) as NumberPicker
np.clearFocus()
presenter.setScore(np.value)
myanimelist_score.text = "..."
}
}
.show()
val view = dialog.customView
if (view != null) {
val np = view.findViewById(R.id.score_picker) as NumberPicker
// Set initial value
np.value = presenter.mangaSync!!.score.toInt()
}
}
}

View File

@ -1,174 +0,0 @@
package eu.kanade.tachiyomi.ui.manga.myanimelist
import android.os.Bundle
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.manga.MangaEvent
import eu.kanade.tachiyomi.util.SharedData
import eu.kanade.tachiyomi.util.toast
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
class MyAnimeListPresenter : BasePresenter<MyAnimeListFragment>() {
val db: DatabaseHelper by injectLazy()
val syncManager: MangaSyncManager by injectLazy()
val myAnimeList by lazy { syncManager.myAnimeList }
lateinit var manga: Manga
private set
var mangaSync: MangaSync? = null
private set
private var query: String? = null
private val GET_MANGA_SYNC = 1
private val GET_SEARCH_RESULTS = 2
private val REFRESH = 3
private val PREFIX_MY = "my:"
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
startableLatestCache(GET_MANGA_SYNC,
{ db.getMangaSync(manga, myAnimeList).asRxObservable()
.doOnNext { mangaSync = it }
.observeOn(AndroidSchedulers.mainThread()) },
{ view, mangaSync -> view.setMangaSync(mangaSync) })
startableLatestCache(GET_SEARCH_RESULTS,
{ getSearchResultsObservable() },
{ view, results -> view.setSearchResults(results) },
{ view, error -> view.setSearchResultsError(error) })
startableFirst(REFRESH,
{ getRefreshObservable() },
{ view, result -> view.onRefreshDone() },
{ view, error -> view.onRefreshError(error) })
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
start(GET_MANGA_SYNC)
}
fun getSearchResultsObservable(): Observable<List<MangaSync>> {
return query?.let { query ->
val observable: Observable<List<MangaSync>>
if (query.startsWith(PREFIX_MY)) {
val realQuery = query.substring(PREFIX_MY.length).toLowerCase().trim()
observable = myAnimeList.getList()
.flatMap { Observable.from(it) }
.filter { it.title.toLowerCase().contains(realQuery) }
.toList()
} else {
observable = myAnimeList.search(query)
}
observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
} ?: Observable.error(Exception("Null query"))
}
fun getRefreshObservable(): Observable<PutResult> {
return mangaSync?.let { mangaSync ->
myAnimeList.getList()
.map { myList ->
myList.find { it.remote_id == mangaSync.remote_id }?.let {
mangaSync.copyPersonalFrom(it)
mangaSync.total_chapters = it.total_chapters
mangaSync
} ?: throw Exception("Could not find manga")
}
.flatMap { db.insertMangaSync(it).asRxObservable() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
} ?: Observable.error(Exception("Not found"))
}
private fun updateRemote() {
mangaSync?.let { mangaSync ->
add(myAnimeList.update(mangaSync)
.subscribeOn(Schedulers.io())
.flatMap { db.insertMangaSync(mangaSync).asRxObservable() }
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ next -> },
{ error ->
Timber.e(error)
// Restart on error to set old values
start(GET_MANGA_SYNC)
}))
}
}
fun searchManga(query: String) {
if (query.isNullOrEmpty() || query == this.query)
return
this.query = query
start(GET_SEARCH_RESULTS)
}
fun restartSearch() {
query = null
stop(GET_SEARCH_RESULTS)
}
fun registerManga(sync: MangaSync?) {
if (sync != null) {
sync.manga_id = manga.id!!
add(myAnimeList.bind(sync)
.flatMap { db.insertMangaSync(sync).asRxObservable() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ },
{ error -> context.toast(error.message) }))
} else {
db.deleteMangaSyncForManga(manga).executeAsBlocking()
}
}
fun getAllStatus(): List<String> {
return listOf(context.getString(R.string.reading),
context.getString(R.string.completed),
context.getString(R.string.on_hold),
context.getString(R.string.dropped),
context.getString(R.string.plan_to_read))
}
fun getIndexFromStatus(): Int {
return mangaSync?.let { mangaSync ->
if (mangaSync.status == 6) 4 else mangaSync.status - 1
} ?: 0
}
fun setStatus(index: Int) {
mangaSync?.status = if (index == 4) 6 else index + 1
updateRemote()
}
fun setScore(score: Int) {
mangaSync?.score = score.toFloat()
updateRemote()
}
fun setLastChapterRead(chapterNumber: Int) {
mangaSync?.last_chapter_read = chapterNumber
updateRemote()
}
fun refresh() {
if (mangaSync != null) {
start(REFRESH)
}
}
}

View File

@ -1,46 +0,0 @@
package eu.kanade.tachiyomi.ui.manga.myanimelist
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.util.inflate
import kotlinx.android.synthetic.main.dialog_myanimelist_search_item.view.*
import java.util.*
class MyAnimeListSearchAdapter(context: Context) :
ArrayAdapter<MangaSync>(context, R.layout.dialog_myanimelist_search_item, ArrayList<MangaSync>()) {
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
var v = view
// Get the data item for this position
val sync = getItem(position)
// Check if an existing view is being reused, otherwise inflate the view
val holder: SearchViewHolder // view lookup cache stored in tag
if (v == null) {
v = parent.inflate(R.layout.dialog_myanimelist_search_item)
holder = SearchViewHolder(v)
v.tag = holder
} else {
holder = v.tag as SearchViewHolder
}
holder.onSetValues(sync)
return v
}
fun setItems(syncs: List<MangaSync>) {
setNotifyOnChange(false)
clear()
addAll(syncs)
notifyDataSetChanged()
}
class SearchViewHolder(private val view: View) {
fun onSetValues(sync: MangaSync) {
view.myanimelist_result_title.text = sync.title
}
}
}

View File

@ -0,0 +1,33 @@
package eu.kanade.tachiyomi.ui.manga.track
import android.support.v7.widget.RecyclerView
import android.view.ViewGroup
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.inflate
class TrackAdapter(val fragment: TrackFragment) : RecyclerView.Adapter<TrackHolder>() {
var items = emptyList<TrackItem>()
set(value) {
if (field !== value) {
field = value
notifyDataSetChanged()
}
}
var onClickListener: (TrackItem) -> Unit = {}
override fun getItemCount(): Int {
return items.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder {
val view = parent.inflate(R.layout.item_track)
return TrackHolder(view, fragment)
}
override fun onBindViewHolder(holder: TrackHolder, position: Int) {
holder.onSetValues(items[position])
}
}

View File

@ -0,0 +1,166 @@
package eu.kanade.tachiyomi.ui.manga.track
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.NumberPicker
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
import eu.kanade.tachiyomi.ui.manga.MangaActivity
import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.fragment_track.*
import nucleus.factory.RequiresPresenter
@RequiresPresenter(TrackPresenter::class)
class TrackFragment : BaseRxFragment<TrackPresenter>() {
companion object {
fun newInstance(): TrackFragment {
return TrackFragment()
}
}
private lateinit var adapter: TrackAdapter
private var dialog: TrackSearchDialog? = null
private val searchFragmentTag: String
get() = "search_fragment"
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View {
return inflater.inflate(R.layout.fragment_track, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
adapter = TrackAdapter(this)
recycler.layoutManager = LinearLayoutManager(context)
recycler.adapter = adapter
swipe_refresh.isEnabled = false
swipe_refresh.setOnRefreshListener { presenter.refresh() }
}
private fun findSearchFragmentIfNeeded() {
if (dialog == null) {
dialog = childFragmentManager.findFragmentByTag(searchFragmentTag) as TrackSearchDialog
}
}
fun onNextTrackings(trackings: List<TrackItem>) {
adapter.items = trackings
swipe_refresh.isEnabled = trackings.any { it.track != null }
(activity as MangaActivity).setTrackingIcon(trackings.any { it.track != null })
}
fun onSearchResults(results: List<Track>) {
if (!isResumed) return
findSearchFragmentIfNeeded()
dialog?.onSearchResults(results)
}
fun onSearchResultsError(error: Throwable) {
if (!isResumed) return
findSearchFragmentIfNeeded()
dialog?.onSearchResultsError()
}
fun onRefreshDone() {
swipe_refresh.isRefreshing = false
}
fun onRefreshError(error: Throwable) {
swipe_refresh.isRefreshing = false
context.toast(error.message)
}
fun onTitleClick(item: TrackItem) {
if (!isResumed) return
if (dialog == null) {
dialog = TrackSearchDialog.newInstance()
}
presenter.selectedService = item.service
dialog?.show(childFragmentManager, searchFragmentTag)
}
fun onStatusClick(item: TrackItem) {
if (!isResumed || item.track == null) return
val statusList = item.service.getStatusList().map { item.service.getStatus(it) }
val selectedIndex = item.service.getStatusList().indexOf(item.track.status)
MaterialDialog.Builder(context)
.title(R.string.status)
.items(statusList)
.itemsCallbackSingleChoice(selectedIndex, { dialog, view, i, charSequence ->
presenter.setStatus(item, i)
swipe_refresh.isRefreshing = true
true
})
.show()
}
fun onChaptersClick(item: TrackItem) {
if (!isResumed || item.track == null) return
val dialog = MaterialDialog.Builder(context)
.title(R.string.chapters)
.customView(R.layout.dialog_track_chapters, false)
.positiveText(android.R.string.ok)
.negativeText(android.R.string.cancel)
.onPositive { d, action ->
val view = d.customView
if (view != null) {
val np = view.findViewById(R.id.chapters_picker) as NumberPicker
np.clearFocus()
presenter.setLastChapterRead(item, np.value)
swipe_refresh.isRefreshing = true
}
}
.show()
val view = dialog.customView
if (view != null) {
val np = view.findViewById(R.id.chapters_picker) as NumberPicker
// Set initial value
np.value = item.track.last_chapter_read
// Don't allow to go from 0 to 9999
np.wrapSelectorWheel = false
}
}
fun onScoreClick(item: TrackItem) {
if (!isResumed || item.track == null) return
val dialog = MaterialDialog.Builder(activity)
.title(R.string.score)
.customView(R.layout.dialog_track_score, false)
.positiveText(android.R.string.ok)
.negativeText(android.R.string.cancel)
.onPositive { d, action ->
val view = d.customView
if (view != null) {
val np = view.findViewById(R.id.score_picker) as NumberPicker
np.clearFocus()
presenter.setScore(item, np.value)
swipe_refresh.isRefreshing = true
}
}
.show()
val view = dialog.customView
if (view != null) {
val np = view.findViewById(R.id.score_picker) as NumberPicker
np.maxValue = item.service.maxScore()
// Set initial value
np.value = item.track.score.toInt()
}
}
}

View File

@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.ui.manga.track
import android.support.v7.widget.RecyclerView
import android.view.View
import eu.kanade.tachiyomi.R
import kotlinx.android.synthetic.main.item_track.view.*
class TrackHolder(private val view: View, private val fragment: TrackFragment)
: RecyclerView.ViewHolder(view) {
private lateinit var item: TrackItem
init {
view.title_container.setOnClickListener { fragment.onTitleClick(item) }
view.status_container.setOnClickListener { fragment.onStatusClick(item) }
view.chapters_container.setOnClickListener { fragment.onChaptersClick(item) }
view.score_container.setOnClickListener { fragment.onScoreClick(item) }
}
@Suppress("DEPRECATION")
fun onSetValues(item: TrackItem) = with(view) {
this@TrackHolder.item = item
val track = item.track
track_logo.setImageResource(item.service.getLogo())
logo.setBackgroundColor(item.service.getLogoColor())
if (track != null) {
track_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary)
track_title.setAllCaps(false)
track_title.text = track.title
track_chapters.text = "${track.last_chapter_read}/" +
if (track.total_chapters > 0) track.total_chapters else "-"
track_status.text = item.service.getStatus(track.status)
track_score.text = if (track.score == 0f) "-" else item.service.formatScore(track)
} else {
track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button)
track_title.setText(R.string.action_edit)
track_chapters.text = ""
track_score.text = ""
track_status.text = ""
}
}
}

View File

@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.ui.manga.track
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackService
class TrackItem(val track: Track?, val service: TrackService) {
}

View File

@ -0,0 +1,137 @@
package eu.kanade.tachiyomi.ui.manga.track
import android.os.Bundle
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.manga.MangaEvent
import eu.kanade.tachiyomi.util.SharedData
import eu.kanade.tachiyomi.util.toast
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
class TrackPresenter : BasePresenter<TrackFragment>() {
private val db: DatabaseHelper by injectLazy()
private val trackManager: TrackManager by injectLazy()
lateinit var manga: Manga
private set
private var trackList: List<TrackItem> = emptyList()
private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
var selectedService: TrackService? = null
private var trackSubscription: Subscription? = null
private var searchSubscription: Subscription? = null
private var refreshSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
fetchTrackings()
}
fun fetchTrackings() {
trackSubscription?.let { remove(it) }
trackSubscription = db.getTracks(manga)
.asRxObservable()
.map { tracks ->
loggedServices.map { service ->
TrackItem(tracks.find { it.sync_id == service.id }, service)
}
}
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { trackList = it }
.subscribeLatestCache(TrackFragment::onNextTrackings)
}
fun refresh() {
refreshSubscription?.let { remove(it) }
refreshSubscription = Observable.from(trackList)
.filter { it.track != null }
.concatMap { item ->
item.service.refresh(item.track!!)
.flatMap { db.insertTrack(it).asRxObservable() }
.map { item }
.onErrorReturn { item }
}
.toList()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst({ view, result -> view.onRefreshDone() },
TrackFragment::onRefreshError)
}
fun search(query: String) {
val service = selectedService ?: return
searchSubscription?.let { remove(it) }
searchSubscription = service.search(query)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache(TrackFragment::onSearchResults,
TrackFragment::onSearchResultsError)
}
fun registerTracking(item: Track?) {
val service = selectedService ?: return
if (item != null) {
item.manga_id = manga.id!!
add(service.bind(item)
.flatMap { db.insertTrack(item).asRxObservable() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ },
{ error -> context.toast(error.message) }))
} else {
db.deleteTrackForManga(manga, service).executeAsBlocking()
}
}
private fun updateRemote(track: Track, service: TrackService) {
service.update(track)
.flatMap { db.insertTrack(track).asRxObservable() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst({ view, result -> view.onRefreshDone() },
{ view, error ->
view.onRefreshError(error)
// Restart on error to set old values
fetchTrackings()
})
}
fun setStatus(item: TrackItem, index: Int) {
val track = item.track!!
track.status = item.service.getStatusList()[index]
updateRemote(track, item.service)
}
fun setScore(item: TrackItem, score: Int) {
val track = item.track!!
track.score = score.toFloat()
updateRemote(track, item.service)
}
fun setLastChapterRead(item: TrackItem, chapterNumber: Int) {
val track = item.track!!
track.last_chapter_read = chapterNumber
updateRemote(track, item.service)
}
}

View File

@ -0,0 +1,47 @@
package eu.kanade.tachiyomi.ui.manga.track
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.util.inflate
import kotlinx.android.synthetic.main.item_track_search.view.*
import java.util.*
class TrackSearchAdapter(context: Context)
: ArrayAdapter<Track>(context, R.layout.item_track_search, ArrayList<Track>()) {
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
var v = view
// Get the data item for this position
val track = getItem(position)
// Check if an existing view is being reused, otherwise inflate the view
val holder: TrackSearchHolder // view lookup cache stored in tag
if (v == null) {
v = parent.inflate(R.layout.item_track_search)
holder = TrackSearchHolder(v)
v.tag = holder
} else {
holder = v.tag as TrackSearchHolder
}
holder.onSetValues(track)
return v
}
fun setItems(syncs: List<Track>) {
setNotifyOnChange(false)
clear()
addAll(syncs)
notifyDataSetChanged()
}
class TrackSearchHolder(private val view: View) {
fun onSetValues(track: Track) {
view.track_search_title.text = track.title
}
}
}

View File

@ -0,0 +1,119 @@
package eu.kanade.tachiyomi.ui.manga.track
import android.app.Dialog
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.view.View
import com.afollestad.materialdialogs.MaterialDialog
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
import kotlinx.android.synthetic.main.dialog_track_search.view.*
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
class TrackSearchDialog : DialogFragment() {
companion object {
fun newInstance(): TrackSearchDialog {
return TrackSearchDialog()
}
}
private lateinit var v: View
lateinit var adapter: TrackSearchAdapter
private set
private val queryRelay by lazy { PublishRelay.create<String>() }
private var searchDebounceSubscription: Subscription? = null
private var selectedItem: Track? = null
val presenter: TrackPresenter
get() = (parentFragment as TrackFragment).presenter
override fun onCreateDialog(savedState: Bundle?): Dialog {
val dialog = MaterialDialog.Builder(context)
.customView(R.layout.dialog_track_search, false)
.positiveText(android.R.string.ok)
.negativeText(android.R.string.cancel)
.onPositive { dialog1, which -> onPositiveButtonClick() }
.build()
onViewCreated(dialog.view, savedState)
return dialog
}
override fun onViewCreated(view: View, savedState: Bundle?) {
v = view
// Create adapter
adapter = TrackSearchAdapter(context)
view.track_search_list.adapter = adapter
// Set listeners
selectedItem = null
view.track_search_list.setOnItemClickListener { parent, viewList, position, id ->
selectedItem = adapter.getItem(position)
}
// Do an initial search based on the manga's title
if (savedState == null) {
val title = presenter.manga.title
view.track_search.append(title)
search(title)
}
view.track_search.addTextChangedListener(object : SimpleTextWatcher() {
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
queryRelay.call(s.toString())
}
})
}
override fun onResume() {
super.onResume()
// Listen to text changes
searchDebounceSubscription = queryRelay.debounce(1, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.filter { it.isNotBlank() }
.subscribe { search(it) }
}
override fun onPause() {
searchDebounceSubscription?.unsubscribe()
super.onPause()
}
private fun search(query: String) {
v.progress.visibility = View.VISIBLE
v.track_search_list.visibility = View.GONE
presenter.search(query)
}
fun onSearchResults(results: List<Track>) {
selectedItem = null
v.progress.visibility = View.GONE
v.track_search_list.visibility = View.VISIBLE
adapter.setItems(results)
}
fun onSearchResultsError() {
v.progress.visibility = View.VISIBLE
v.track_search_list.visibility = View.GONE
adapter.setItems(emptyList())
}
private fun onPositiveButtonClick() {
presenter.registerTracking(selectedItem)
}
}

View File

@ -163,19 +163,19 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
}
override fun onBackPressed() {
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 {

View File

@ -10,14 +10,14 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.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<ReaderActivity>() {
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<ReaderActivity>() {
/**
* List of manga services linked to the active manga, or null if auto syncing is not enabled.
*/
private var mangaSyncList: List<MangaSync>? = null
private var trackList: List<Track>? = null
/**
* Chapter loader whose job is to obtain the chapter list and initialize every page.
@ -165,9 +165,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
.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<ReaderActivity>() {
/**
* 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<ReaderActivity>() {
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)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -3,20 +3,20 @@ package eu.kanade.tachiyomi.widget.preference
import android.os.Bundle
import android.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)
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
</vector>

View File

@ -1,149 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/cv_mal"
style="@style/Theme.Widget.CardView"
>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/card_margin">
<RelativeLayout
android:id="@+id/myanimelist_title_layout"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?attr/selectable_list_drawable"
android:clickable="true"
android:paddingLeft="?android:listPreferredItemPaddingLeft"
android:paddingRight="?android:listPreferredItemPaddingRight">
<TextView
style="@style/TextAppearance.Regular.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="Title"/>
<TextView
android:id="@+id/myanimelist_title"
style="@style/TextAppearance.Medium.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:text="@string/action_edit"/>
</RelativeLayout>
<View
android:id="@+id/divider1"
android:layout_width="fill_parent"
android:layout_height="1dp"
android:layout_below="@id/myanimelist_title_layout"
android:background="?android:attr/divider"/>
<RelativeLayout
android:id="@+id/myanimelist_status_layout"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:layout_below="@id/divider1"
android:background="?attr/selectable_list_drawable"
android:clickable="true"
android:paddingLeft="?android:listPreferredItemPaddingLeft"
android:paddingRight="?android:listPreferredItemPaddingRight"
>
<TextView
style="@style/TextAppearance.Regular.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="Status"/>
<TextView
android:id="@+id/myanimelist_status"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
tools:text="Reading"/>
</RelativeLayout>
<View
android:id="@+id/divider2"
android:layout_width="fill_parent"
android:layout_height="1dp"
android:layout_below="@id/myanimelist_status_layout"
android:background="?android:attr/divider"/>
<RelativeLayout
android:id="@+id/myanimelist_chapters_layout"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:layout_below="@id/divider2"
android:background="?attr/selectable_list_drawable"
android:clickable="true"
android:paddingLeft="?android:listPreferredItemPaddingLeft"
android:paddingRight="?android:listPreferredItemPaddingRight">
<TextView
style="@style/TextAppearance.Regular.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="Chapters"/>
<TextView
android:id="@+id/myanimelist_chapters"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
tools:text="12/24"/>
</RelativeLayout>
<View
android:id="@+id/divider3"
android:layout_width="fill_parent"
android:layout_height="1dp"
android:layout_below="@id/myanimelist_chapters_layout"
android:background="?android:attr/divider"/>
<RelativeLayout
android:id="@+id/myanimelist_score_layout"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:layout_below="@id/divider3"
android:background="?attr/selectable_list_drawable"
android:clickable="true"
android:paddingLeft="?android:listPreferredItemPaddingLeft"
android:paddingRight="?android:listPreferredItemPaddingRight">
<TextView
style="@style/TextAppearance.Regular.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="@string/score"/>
<TextView
android:id="@+id/myanimelist_score"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
tools:text="10"/>
</RelativeLayout>
</RelativeLayout>
</android.support.v7.widget.CardView>

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?attr/selectable_list_drawable">
<TextView
android:id="@+id/myanimelist_result_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"/>
</LinearLayout>

View File

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

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/card_myanimelist_personal"/>
</ScrollView>
</android.support.v4.widget.SwipeRefreshLayout>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout>

View File

@ -0,0 +1,185 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/track"
style="@style/Theme.Widget.CardView">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:id="@+id/logo"
android:layout_width="48dp"
android:layout_height="0dp"
tools:background="#2E51A2"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<ImageView
android:id="@+id/track_logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
tools:src="@drawable/mal" />
</FrameLayout>
<RelativeLayout
android:id="@+id/title_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="?attr/selectable_list_drawable"
android:clickable="true"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@+id/logo"
app:layout_constraintRight_toRightOf="parent">
<TextView
style="@style/TextAppearance.Regular.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title" />
<TextView
android:id="@+id/track_title"
style="@style/TextAppearance.Medium.Button"
android:textColor="?colorAccent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:text="@string/action_edit" />
</RelativeLayout>
<View
android:id="@+id/divider1"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?android:attr/divider"
app:layout_constraintTop_toBottomOf="@+id/title_container"
app:layout_constraintLeft_toRightOf="@+id/logo"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp" />
<RelativeLayout
android:id="@+id/status_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="?attr/selectable_list_drawable"
android:clickable="true"
app:layout_constraintTop_toBottomOf="@+id/divider1"
app:layout_constraintLeft_toRightOf="@+id/logo"
app:layout_constraintRight_toRightOf="parent">
<TextView
style="@style/TextAppearance.Regular.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status" />
<TextView
android:id="@+id/track_status"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
tools:text="Reading" />
</RelativeLayout>
<View
android:id="@+id/divider2"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?android:attr/divider"
app:layout_constraintTop_toBottomOf="@+id/status_container"
app:layout_constraintLeft_toRightOf="@+id/logo"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp" />
<RelativeLayout
android:id="@+id/chapters_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="?attr/selectable_list_drawable"
android:clickable="true"
app:layout_constraintTop_toBottomOf="@+id/divider2"
app:layout_constraintLeft_toRightOf="@+id/logo"
app:layout_constraintRight_toRightOf="parent">
<TextView
style="@style/TextAppearance.Regular.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/chapters" />
<TextView
android:id="@+id/track_chapters"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
tools:text="12/24" />
</RelativeLayout>
<View
android:id="@+id/divider3"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?android:attr/divider"
app:layout_constraintTop_toBottomOf="@+id/chapters_container"
app:layout_constraintLeft_toRightOf="@+id/logo"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp" />
<RelativeLayout
android:id="@+id/score_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="?attr/selectable_list_drawable"
android:clickable="true"
app:layout_constraintTop_toBottomOf="@+id/divider3"
app:layout_constraintLeft_toRightOf="@+id/logo"
app:layout_constraintRight_toRightOf="parent">
<TextView
style="@style/TextAppearance.Regular.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/score" />
<TextView
android:id="@+id/track_score"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
tools:text="10" />
</RelativeLayout>
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="?attr/selectable_list_drawable">
<TextView
android:id="@+id/track_search_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"/>
</LinearLayout>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<JsonElement>()
@ -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<JsonElement>()
@ -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<JsonElement>()
@ -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<MangaSync> {
val ms = ArrayList<MangaSync>()
private fun createTrack(manga: Manga, vararg syncIds: Int): List<Track> {
val ms = ArrayList<Track>()
for (title in syncIds) {
ms.add(createMangaSync(manga, title))
ms.add(createTrack(manga, title))
}
return ms
}