mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-22 18:51:49 +01:00
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:
parent
e3d430eb5e
commit
94ee4e7fb5
12
.travis.yml
12
.travis.yml
@ -12,11 +12,21 @@ android:
|
|||||||
- extra-android-support
|
- extra-android-support
|
||||||
- extra-google-google_play_services
|
- extra-google-google_play_services
|
||||||
|
|
||||||
|
licenses:
|
||||||
|
- android-sdk-license-.+
|
||||||
|
- '.+'
|
||||||
|
|
||||||
jdk:
|
jdk:
|
||||||
- oraclejdk8
|
- oraclejdk8
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- chmod +x gradlew
|
- chmod +x gradlew
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- mkdir "$ANDROID_HOME/licenses" || true
|
||||||
|
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license"
|
||||||
|
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
|
||||||
|
|
||||||
#Build, and run tests
|
#Build, and run tests
|
||||||
script: "./gradlew clean buildStandardDebug"
|
script: "./gradlew clean buildStandardDebug"
|
||||||
sudo: false
|
sudo: false
|
||||||
|
@ -110,6 +110,8 @@ dependencies {
|
|||||||
compile "com.android.support:support-annotations:$support_library_version"
|
compile "com.android.support:support-annotations:$support_library_version"
|
||||||
compile "com.android.support:customtabs:$support_library_version"
|
compile "com.android.support:customtabs:$support_library_version"
|
||||||
|
|
||||||
|
compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4'
|
||||||
|
|
||||||
compile 'com.android.support:multidex:1.0.1'
|
compile 'com.android.support:multidex:1.0.1'
|
||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
|
@ -53,6 +53,18 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/FilePickerTheme">
|
android:theme="@style/FilePickerTheme">
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.setting.AnilistLoginActivity"
|
||||||
|
android:label="Anilist">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data
|
||||||
|
android:host="anilist-auth"
|
||||||
|
android:scheme="tachiyomi" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="android.support.v4.content.FileProvider"
|
android:name="android.support.v4.content.FileProvider"
|
||||||
@ -70,7 +82,7 @@
|
|||||||
<service android:name=".data.download.DownloadService"
|
<service android:name=".data.download.DownloadService"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<service android:name=".data.mangasync.UpdateMangaSyncService"
|
<service android:name=".data.track.TrackUpdateService"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<service android:name=".data.updater.UpdateDownloaderService"
|
<service android:name=".data.updater.UpdateDownloaderService"
|
||||||
|
@ -20,7 +20,7 @@ import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
|||||||
reportType = org.acra.sender.HttpSender.Type.JSON,
|
reportType = org.acra.sender.HttpSender.Type.JSON,
|
||||||
httpMethod = org.acra.sender.HttpSender.Method.PUT,
|
httpMethod = org.acra.sender.HttpSender.Method.PUT,
|
||||||
buildConfigClass = BuildConfig::class,
|
buildConfigClass = BuildConfig::class,
|
||||||
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*")
|
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*", ".*token.*")
|
||||||
)
|
)
|
||||||
open class App : Application() {
|
open class App : Application() {
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
|
|||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
@ -32,7 +32,7 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
|
|
||||||
addSingletonFactory { DownloadManager(app) }
|
addSingletonFactory { DownloadManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { MangaSyncManager(app) }
|
addSingletonFactory { TrackManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { Gson() }
|
addSingletonFactory { Gson() }
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
private val MANGA = "manga"
|
private val MANGA = "manga"
|
||||||
private val MANGAS = "mangas"
|
private val MANGAS = "mangas"
|
||||||
private val CHAPTERS = "chapters"
|
private val CHAPTERS = "chapters"
|
||||||
private val MANGA_SYNC = "sync"
|
private val TRACK = "sync"
|
||||||
private val CATEGORIES = "categories"
|
private val CATEGORIES = "categories"
|
||||||
|
|
||||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||||
@ -109,10 +109,10 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
entry.add(CHAPTERS, gson.toJsonTree(chapters))
|
entry.add(CHAPTERS, gson.toJsonTree(chapters))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backup manga sync
|
// Backup tracks
|
||||||
val mangaSync = db.getMangasSync(manga).executeAsBlocking()
|
val tracks = db.getTracks(manga).executeAsBlocking()
|
||||||
if (!mangaSync.isEmpty()) {
|
if (!tracks.isEmpty()) {
|
||||||
entry.add(MANGA_SYNC, gson.toJsonTree(mangaSync))
|
entry.add(TRACK, gson.toJsonTree(tracks))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backup categories for this manga
|
// Backup categories for this manga
|
||||||
@ -231,13 +231,13 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
val element = backupManga.asJsonObject
|
val element = backupManga.asJsonObject
|
||||||
val manga = gson.fromJson(element.get(MANGA), MangaImpl::class.java)
|
val manga = gson.fromJson(element.get(MANGA), MangaImpl::class.java)
|
||||||
val chapters = gson.fromJson<List<ChapterImpl>>(element.get(CHAPTERS) ?: JsonArray())
|
val chapters = gson.fromJson<List<ChapterImpl>>(element.get(CHAPTERS) ?: JsonArray())
|
||||||
val sync = gson.fromJson<List<MangaSyncImpl>>(element.get(MANGA_SYNC) ?: JsonArray())
|
val tracks = gson.fromJson<List<TrackImpl>>(element.get(TRACK) ?: JsonArray())
|
||||||
val categories = gson.fromJson<List<String>>(element.get(CATEGORIES) ?: JsonArray())
|
val categories = gson.fromJson<List<String>>(element.get(CATEGORIES) ?: JsonArray())
|
||||||
|
|
||||||
// Restore everything related to this manga
|
// Restore everything related to this manga
|
||||||
restoreManga(manga)
|
restoreManga(manga)
|
||||||
restoreChaptersForManga(manga, chapters)
|
restoreChaptersForManga(manga, chapters)
|
||||||
restoreSyncForManga(manga, sync)
|
restoreSyncForManga(manga, tracks)
|
||||||
restoreCategoriesForManga(manga, categories)
|
restoreCategoriesForManga(manga, categories)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -333,35 +333,35 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
* Restores the sync of a manga.
|
* Restores the sync of a manga.
|
||||||
*
|
*
|
||||||
* @param manga the manga whose sync have to be restored.
|
* @param manga the manga whose sync have to be restored.
|
||||||
* @param sync the sync to restore.
|
* @param tracks the track list to restore.
|
||||||
*/
|
*/
|
||||||
private fun restoreSyncForManga(manga: Manga, sync: List<MangaSync>) {
|
private fun restoreSyncForManga(manga: Manga, tracks: List<Track>) {
|
||||||
// Fix foreign keys with the current manga id
|
// Fix foreign keys with the current manga id
|
||||||
for (mangaSync in sync) {
|
for (track in tracks) {
|
||||||
mangaSync.manga_id = manga.id!!
|
track.manga_id = manga.id!!
|
||||||
}
|
}
|
||||||
|
|
||||||
val dbSyncs = db.getMangasSync(manga).executeAsBlocking()
|
val dbTracks = db.getTracks(manga).executeAsBlocking()
|
||||||
val syncToUpdate = ArrayList<MangaSync>()
|
val trackToUpdate = ArrayList<Track>()
|
||||||
for (backupSync in sync) {
|
for (backupTrack in tracks) {
|
||||||
// Try to find existing chapter in db
|
// Try to find existing chapter in db
|
||||||
val pos = dbSyncs.indexOf(backupSync)
|
val pos = dbTracks.indexOf(backupTrack)
|
||||||
if (pos != -1) {
|
if (pos != -1) {
|
||||||
// The sync is already in the db, only update its fields
|
// The sync is already in the db, only update its fields
|
||||||
val dbSync = dbSyncs[pos]
|
val dbSync = dbTracks[pos]
|
||||||
// Mark the max chapter as read and nothing else
|
// Mark the max chapter as read and nothing else
|
||||||
dbSync.last_chapter_read = Math.max(backupSync.last_chapter_read, dbSync.last_chapter_read)
|
dbSync.last_chapter_read = Math.max(backupTrack.last_chapter_read, dbSync.last_chapter_read)
|
||||||
syncToUpdate.add(dbSync)
|
trackToUpdate.add(dbSync)
|
||||||
} else {
|
} else {
|
||||||
// Insert new sync. Let the db assign the id
|
// Insert new sync. Let the db assign the id
|
||||||
backupSync.id = null
|
backupTrack.id = null
|
||||||
syncToUpdate.add(backupSync)
|
trackToUpdate.add(backupTrack)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update database
|
// Update database
|
||||||
if (!syncToUpdate.isEmpty()) {
|
if (!trackToUpdate.isEmpty()) {
|
||||||
db.insertMangasSync(syncToUpdate).executeAsBlocking()
|
db.insertTracks(trackToUpdate).executeAsBlocking()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import com.google.gson.FieldAttributes
|
|||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSyncImpl
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
|
||||||
class IdExclusion : ExclusionStrategy {
|
class IdExclusion : ExclusionStrategy {
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ class IdExclusion : ExclusionStrategy {
|
|||||||
override fun shouldSkipField(f: FieldAttributes) = when (f.declaringClass) {
|
override fun shouldSkipField(f: FieldAttributes) = when (f.declaringClass) {
|
||||||
MangaImpl::class.java -> mangaExclusions.contains(f.name)
|
MangaImpl::class.java -> mangaExclusions.contains(f.name)
|
||||||
ChapterImpl::class.java -> chapterExclusions.contains(f.name)
|
ChapterImpl::class.java -> chapterExclusions.contains(f.name)
|
||||||
MangaSyncImpl::class.java -> syncExclusions.contains(f.name)
|
TrackImpl::class.java -> syncExclusions.contains(f.name)
|
||||||
CategoryImpl::class.java -> categoryExclusions.contains(f.name)
|
CategoryImpl::class.java -> categoryExclusions.contains(f.name)
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
@ -10,13 +10,13 @@ import eu.kanade.tachiyomi.data.database.queries.*
|
|||||||
* This class provides operations to manage the database through its interfaces.
|
* This class provides operations to manage the database through its interfaces.
|
||||||
*/
|
*/
|
||||||
open class DatabaseHelper(context: Context)
|
open class DatabaseHelper(context: Context)
|
||||||
: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
: MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
||||||
|
|
||||||
override val db = DefaultStorIOSQLite.builder()
|
override val db = DefaultStorIOSQLite.builder()
|
||||||
.sqliteOpenHelper(DbOpenHelper(context))
|
.sqliteOpenHelper(DbOpenHelper(context))
|
||||||
.addTypeMapping(Manga::class.java, MangaTypeMapping())
|
.addTypeMapping(Manga::class.java, MangaTypeMapping())
|
||||||
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
|
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
|
||||||
.addTypeMapping(MangaSync::class.java, MangaSyncTypeMapping())
|
.addTypeMapping(Track::class.java, TrackTypeMapping())
|
||||||
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
||||||
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
||||||
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
||||||
|
@ -23,7 +23,7 @@ class DbOpenHelper(context: Context)
|
|||||||
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
||||||
execSQL(MangaTable.createTableQuery)
|
execSQL(MangaTable.createTableQuery)
|
||||||
execSQL(ChapterTable.createTableQuery)
|
execSQL(ChapterTable.createTableQuery)
|
||||||
execSQL(MangaSyncTable.createTableQuery)
|
execSQL(TrackTable.createTableQuery)
|
||||||
execSQL(CategoryTable.createTableQuery)
|
execSQL(CategoryTable.createTableQuery)
|
||||||
execSQL(MangaCategoryTable.createTableQuery)
|
execSQL(MangaCategoryTable.createTableQuery)
|
||||||
execSQL(HistoryTable.createTableQuery)
|
execSQL(HistoryTable.createTableQuery)
|
||||||
|
@ -9,38 +9,38 @@ import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
|||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSyncImpl
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_ID
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_LAST_CHAPTER_READ
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_MANGA_ID
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_REMOTE_ID
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_REMOTE_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_SCORE
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_STATUS
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_SYNC_ID
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_TITLE
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_TOTAL_CHAPTERS
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TOTAL_CHAPTERS
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.TABLE
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
|
||||||
|
|
||||||
class MangaSyncTypeMapping : SQLiteTypeMapping<MangaSync>(
|
class TrackTypeMapping : SQLiteTypeMapping<Track>(
|
||||||
MangaSyncPutResolver(),
|
TrackPutResolver(),
|
||||||
MangaSyncGetResolver(),
|
TrackGetResolver(),
|
||||||
MangaSyncDeleteResolver()
|
TrackDeleteResolver()
|
||||||
)
|
)
|
||||||
|
|
||||||
class MangaSyncPutResolver : DefaultPutResolver<MangaSync>() {
|
class TrackPutResolver : DefaultPutResolver<Track>() {
|
||||||
|
|
||||||
override fun mapToInsertQuery(obj: MangaSync) = InsertQuery.builder()
|
override fun mapToInsertQuery(obj: Track) = InsertQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToUpdateQuery(obj: MangaSync) = UpdateQuery.builder()
|
override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.where("$COL_ID = ?")
|
.where("$COL_ID = ?")
|
||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: MangaSync) = ContentValues(9).apply {
|
override fun mapToContentValues(obj: Track) = ContentValues(9).apply {
|
||||||
put(COL_ID, obj.id)
|
put(COL_ID, obj.id)
|
||||||
put(COL_MANGA_ID, obj.manga_id)
|
put(COL_MANGA_ID, obj.manga_id)
|
||||||
put(COL_SYNC_ID, obj.sync_id)
|
put(COL_SYNC_ID, obj.sync_id)
|
||||||
@ -53,9 +53,9 @@ class MangaSyncPutResolver : DefaultPutResolver<MangaSync>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MangaSyncGetResolver : DefaultGetResolver<MangaSync>() {
|
class TrackGetResolver : DefaultGetResolver<Track>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): MangaSync = MangaSyncImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||||
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
|
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
|
||||||
@ -68,9 +68,9 @@ class MangaSyncGetResolver : DefaultGetResolver<MangaSync>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MangaSyncDeleteResolver : DefaultDeleteResolver<MangaSync>() {
|
class TrackDeleteResolver : DefaultDeleteResolver<Track>() {
|
||||||
|
|
||||||
override fun mapToDeleteQuery(obj: MangaSync) = DeleteQuery.builder()
|
override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder()
|
||||||
.table(TABLE)
|
.table(TABLE)
|
||||||
.where("$COL_ID = ?")
|
.where("$COL_ID = ?")
|
||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.database.models
|
|||||||
|
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface MangaSync : Serializable {
|
interface Track : Serializable {
|
||||||
|
|
||||||
var id: Long?
|
var id: Long?
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ interface MangaSync : Serializable {
|
|||||||
|
|
||||||
var update: Boolean
|
var update: Boolean
|
||||||
|
|
||||||
fun copyPersonalFrom(other: MangaSync) {
|
fun copyPersonalFrom(other: Track) {
|
||||||
last_chapter_read = other.last_chapter_read
|
last_chapter_read = other.last_chapter_read
|
||||||
score = other.score
|
score = other.score
|
||||||
status = other.status
|
status = other.status
|
||||||
@ -32,7 +32,7 @@ interface MangaSync : Serializable {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun create(serviceId: Int): MangaSync = MangaSyncImpl().apply {
|
fun create(serviceId: Int): Track = TrackImpl().apply {
|
||||||
sync_id = serviceId
|
sync_id = serviceId
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
class MangaSyncImpl : MangaSync {
|
class TrackImpl : Track {
|
||||||
|
|
||||||
override var id: Long? = null
|
override var id: Long? = null
|
||||||
|
|
||||||
@ -26,11 +26,11 @@ class MangaSyncImpl : MangaSync {
|
|||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other == null || javaClass != other.javaClass) return false
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
val mangaSync = other as MangaSync
|
other as Track
|
||||||
|
|
||||||
if (manga_id != mangaSync.manga_id) return false
|
if (manga_id != other.manga_id) return false
|
||||||
if (sync_id != mangaSync.sync_id) return false
|
if (sync_id != other.sync_id) return false
|
||||||
return remote_id == mangaSync.remote_id
|
return remote_id == other.remote_id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
@ -1,46 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
|
||||||
|
|
||||||
interface MangaSyncQueries : DbProvider {
|
|
||||||
|
|
||||||
fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get()
|
|
||||||
.`object`(MangaSync::class.java)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(MangaSyncTable.TABLE)
|
|
||||||
.where("${MangaSyncTable.COL_MANGA_ID} = ? AND " +
|
|
||||||
"${MangaSyncTable.COL_SYNC_ID} = ?")
|
|
||||||
.whereArgs(manga.id, sync.id)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getMangasSync(manga: Manga) = db.get()
|
|
||||||
.listOfObjects(MangaSync::class.java)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(MangaSyncTable.TABLE)
|
|
||||||
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare()
|
|
||||||
|
|
||||||
fun insertMangasSync(mangas: List<MangaSync>) = db.put().objects(mangas).prepare()
|
|
||||||
|
|
||||||
fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
|
|
||||||
|
|
||||||
fun deleteMangaSyncForManga(manga: Manga) = db.delete()
|
|
||||||
.byQuery(DeleteQuery.builder()
|
|
||||||
.table(MangaSyncTable.TABLE)
|
|
||||||
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,34 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
|
||||||
|
interface TrackQueries : DbProvider {
|
||||||
|
|
||||||
|
fun getTracks(manga: Manga) = db.get()
|
||||||
|
.listOfObjects(Track::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(TrackTable.TABLE)
|
||||||
|
.where("${TrackTable.COL_MANGA_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun insertTrack(track: Track) = db.put().`object`(track).prepare()
|
||||||
|
|
||||||
|
fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare()
|
||||||
|
|
||||||
|
fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete()
|
||||||
|
.byQuery(DeleteQuery.builder()
|
||||||
|
.table(TrackTable.TABLE)
|
||||||
|
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
|
||||||
|
.whereArgs(manga.id, sync.id)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.tables
|
package eu.kanade.tachiyomi.data.database.tables
|
||||||
|
|
||||||
object MangaSyncTable {
|
object TrackTable {
|
||||||
|
|
||||||
const val TABLE = "manga_sync"
|
const val TABLE = "manga_sync"
|
||||||
|
|
@ -1,23 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.myanimelist.MyAnimeList
|
|
||||||
|
|
||||||
class MangaSyncManager(private val context: Context) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val MYANIMELIST = 1
|
|
||||||
const val ANILIST = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
|
||||||
|
|
||||||
val aniList = Anilist(context, ANILIST)
|
|
||||||
|
|
||||||
// TODO enable anilist
|
|
||||||
val services = listOf(myAnimeList)
|
|
||||||
|
|
||||||
fun getService(id: Int) = services.find { it.id == id }
|
|
||||||
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.support.annotation.CallSuper
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import rx.Completable
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
abstract class MangaSyncService(private val context: Context, val id: Int) {
|
|
||||||
|
|
||||||
val preferences: PreferencesHelper by injectLazy()
|
|
||||||
val networkService: NetworkHelper by injectLazy()
|
|
||||||
|
|
||||||
open val client: OkHttpClient
|
|
||||||
get() = networkService.client
|
|
||||||
|
|
||||||
// Name of the manga sync service to display
|
|
||||||
abstract val name: String
|
|
||||||
|
|
||||||
abstract fun login(username: String, password: String): Completable
|
|
||||||
|
|
||||||
open val isLogged: Boolean
|
|
||||||
get() = !getUsername().isEmpty() &&
|
|
||||||
!getPassword().isEmpty()
|
|
||||||
|
|
||||||
abstract fun add(manga: MangaSync): Observable<MangaSync>
|
|
||||||
|
|
||||||
abstract fun update(manga: MangaSync): Observable<MangaSync>
|
|
||||||
|
|
||||||
abstract fun bind(manga: MangaSync): Observable<MangaSync>
|
|
||||||
|
|
||||||
abstract fun getStatus(status: Int): String
|
|
||||||
|
|
||||||
fun saveCredentials(username: String, password: String) {
|
|
||||||
preferences.setMangaSyncCredentials(this, username, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
open fun logout() {
|
|
||||||
preferences.setMangaSyncCredentials(this, "", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getUsername() = preferences.mangaSyncUsername(this)
|
|
||||||
|
|
||||||
fun getPassword() = preferences.mangaSyncPassword(this)
|
|
||||||
|
|
||||||
}
|
|
@ -1,132 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.anilist
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
|
||||||
import rx.Completable
|
|
||||||
import rx.Observable
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class Anilist(private val context: Context, id: Int) : MangaSyncService(context, id) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val READING = 1
|
|
||||||
const val COMPLETED = 2
|
|
||||||
const val ON_HOLD = 3
|
|
||||||
const val DROPPED = 4
|
|
||||||
const val PLAN_TO_READ = 5
|
|
||||||
|
|
||||||
const val DEFAULT_STATUS = READING
|
|
||||||
const val DEFAULT_SCORE = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override val name = "AniList"
|
|
||||||
|
|
||||||
private val interceptor by lazy { AnilistInterceptor(getPassword()) }
|
|
||||||
|
|
||||||
private val api by lazy {
|
|
||||||
AnilistApi.createService(networkService.client.newBuilder()
|
|
||||||
.addInterceptor(interceptor)
|
|
||||||
.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun login(username: String, password: String) = login(password)
|
|
||||||
|
|
||||||
fun login(authCode: String): Completable {
|
|
||||||
// Create a new api with the default client to avoid request interceptions.
|
|
||||||
return AnilistApi.createService(client)
|
|
||||||
// Request the access token from the API with the authorization code.
|
|
||||||
.requestAccessToken(authCode)
|
|
||||||
// Save the token in the interceptor.
|
|
||||||
.doOnNext { interceptor.setAuth(it) }
|
|
||||||
// Obtain the authenticated user from the API.
|
|
||||||
.zipWith(api.getCurrentUser().map { it["id"].toString() })
|
|
||||||
{ oauth, user -> Pair(user, oauth.refresh_token!!) }
|
|
||||||
// Save service credentials (username and refresh token).
|
|
||||||
.doOnNext { saveCredentials(it.first, it.second) }
|
|
||||||
// Logout on any error.
|
|
||||||
.doOnError { logout() }
|
|
||||||
.toCompletable()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun logout() {
|
|
||||||
super.logout()
|
|
||||||
interceptor.setAuth(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun search(query: String): Observable<List<MangaSync>> {
|
|
||||||
return api.search(query, 1)
|
|
||||||
.flatMap { Observable.from(it) }
|
|
||||||
.filter { it.type != "Novel" }
|
|
||||||
.map { it.toMangaSync() }
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getList(): Observable<List<MangaSync>> {
|
|
||||||
return api.getList(getUsername())
|
|
||||||
.flatMap { Observable.from(it.flatten()) }
|
|
||||||
.map { it.toMangaSync() }
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun add(manga: MangaSync): Observable<MangaSync> {
|
|
||||||
return api.addManga(manga.remote_id, manga.last_chapter_read, manga.getAnilistStatus(),
|
|
||||||
manga.score.toInt())
|
|
||||||
.doOnNext { it.body().close() }
|
|
||||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
|
|
||||||
.doOnError { Timber.e(it, it.message) }
|
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun update(manga: MangaSync): Observable<MangaSync> {
|
|
||||||
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
|
|
||||||
manga.status = COMPLETED
|
|
||||||
}
|
|
||||||
return api.updateManga(manga.remote_id, manga.last_chapter_read, manga.getAnilistStatus(),
|
|
||||||
manga.score.toInt())
|
|
||||||
.doOnNext { it.body().close() }
|
|
||||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
|
|
||||||
.doOnError { Timber.e(it, it.message) }
|
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bind(manga: MangaSync): Observable<MangaSync> {
|
|
||||||
return getList()
|
|
||||||
.flatMap { userlist ->
|
|
||||||
manga.sync_id = id
|
|
||||||
val mangaFromList = userlist.find { it.remote_id == manga.remote_id }
|
|
||||||
if (mangaFromList != null) {
|
|
||||||
manga.copyPersonalFrom(mangaFromList)
|
|
||||||
update(manga)
|
|
||||||
} else {
|
|
||||||
// Set default fields if it's not found in the list
|
|
||||||
manga.score = DEFAULT_SCORE.toFloat()
|
|
||||||
manga.status = DEFAULT_STATUS
|
|
||||||
add(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getStatus(status: Int): String = with(context) {
|
|
||||||
when (status) {
|
|
||||||
READING -> getString(R.string.reading)
|
|
||||||
COMPLETED -> getString(R.string.completed)
|
|
||||||
ON_HOLD -> getString(R.string.on_hold)
|
|
||||||
DROPPED -> getString(R.string.dropped)
|
|
||||||
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun MangaSync.getAnilistStatus() = when (status) {
|
|
||||||
READING -> "reading"
|
|
||||||
COMPLETED -> "completed"
|
|
||||||
ON_HOLD -> "on-hold"
|
|
||||||
DROPPED -> "dropped"
|
|
||||||
PLAN_TO_READ -> "plan to read"
|
|
||||||
else -> throw NotImplementedError("Unknown status")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -51,9 +51,9 @@ class PreferenceKeys(context: Context) {
|
|||||||
|
|
||||||
val updateOnlyNonCompleted = context.getString(R.string.pref_update_only_non_completed_key)
|
val updateOnlyNonCompleted = context.getString(R.string.pref_update_only_non_completed_key)
|
||||||
|
|
||||||
val autoUpdateMangaSync = context.getString(R.string.pref_auto_update_manga_sync_key)
|
val autoUpdateTrack = context.getString(R.string.pref_auto_update_manga_sync_key)
|
||||||
|
|
||||||
val askUpdateMangaSync = context.getString(R.string.pref_ask_update_manga_sync_key)
|
val askUpdateTrack = context.getString(R.string.pref_ask_update_manga_sync_key)
|
||||||
|
|
||||||
val lastUsedCatalogueSource = context.getString(R.string.pref_last_catalogue_source_key)
|
val lastUsedCatalogueSource = context.getString(R.string.pref_last_catalogue_source_key)
|
||||||
|
|
||||||
@ -95,9 +95,11 @@ class PreferenceKeys(context: Context) {
|
|||||||
|
|
||||||
fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId"
|
fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId"
|
||||||
|
|
||||||
fun syncUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||||
|
|
||||||
fun syncPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||||
|
|
||||||
|
fun trackToken(syncId: Int) = "track_token_$syncId"
|
||||||
|
|
||||||
val libraryAsList = context.getString(R.string.pref_display_library_as_list)
|
val libraryAsList = context.getString(R.string.pref_display_library_as_list)
|
||||||
|
|
||||||
|
@ -7,8 +7,8 @@ import android.preference.PreferenceManager
|
|||||||
import com.f2prateek.rx.preferences.Preference
|
import com.f2prateek.rx.preferences.Preference
|
||||||
import com.f2prateek.rx.preferences.RxSharedPreferences
|
import com.f2prateek.rx.preferences.RxSharedPreferences
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
|
||||||
import eu.kanade.tachiyomi.data.source.Source
|
import eu.kanade.tachiyomi.data.source.Source
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
|
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
|
||||||
@ -70,9 +70,9 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun updateOnlyNonCompleted() = prefs.getBoolean(keys.updateOnlyNonCompleted, false)
|
fun updateOnlyNonCompleted() = prefs.getBoolean(keys.updateOnlyNonCompleted, false)
|
||||||
|
|
||||||
fun autoUpdateMangaSync() = prefs.getBoolean(keys.autoUpdateMangaSync, true)
|
fun autoUpdateTrack() = prefs.getBoolean(keys.autoUpdateTrack, true)
|
||||||
|
|
||||||
fun askUpdateMangaSync() = prefs.getBoolean(keys.askUpdateMangaSync, false)
|
fun askUpdateTrack() = prefs.getBoolean(keys.askUpdateTrack, false)
|
||||||
|
|
||||||
fun lastUsedCatalogueSource() = rxPrefs.getInteger(keys.lastUsedCatalogueSource, -1)
|
fun lastUsedCatalogueSource() = rxPrefs.getInteger(keys.lastUsedCatalogueSource, -1)
|
||||||
|
|
||||||
@ -95,17 +95,21 @@ class PreferencesHelper(val context: Context) {
|
|||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mangaSyncUsername(sync: MangaSyncService) = prefs.getString(keys.syncUsername(sync.id), "")
|
fun trackUsername(sync: TrackService) = prefs.getString(keys.trackUsername(sync.id), "")
|
||||||
|
|
||||||
fun mangaSyncPassword(sync: MangaSyncService) = prefs.getString(keys.syncPassword(sync.id), "")
|
fun trackPassword(sync: TrackService) = prefs.getString(keys.trackPassword(sync.id), "")
|
||||||
|
|
||||||
fun setMangaSyncCredentials(sync: MangaSyncService, username: String, password: String) {
|
fun setTrackCredentials(sync: TrackService, username: String, password: String) {
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString(keys.syncUsername(sync.id), username)
|
.putString(keys.trackUsername(sync.id), username)
|
||||||
.putString(keys.syncPassword(sync.id), password)
|
.putString(keys.trackPassword(sync.id), password)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun trackToken(sync: TrackService) = rxPrefs.getString(keys.trackToken(sync.id), "")
|
||||||
|
|
||||||
|
fun anilistScoreType() = rxPrefs.getInteger("anilist_score_type", 0)
|
||||||
|
|
||||||
fun downloadsDirectory() = rxPrefs.getString(keys.downloadsDirectory, defaultDownloadsDir.toString())
|
fun downloadsDirectory() = rxPrefs.getString(keys.downloadsDirectory, defaultDownloadsDir.toString())
|
||||||
|
|
||||||
fun downloadThreads() = rxPrefs.getInteger(keys.downloadThreads, 1)
|
fun downloadThreads() = rxPrefs.getInteger(keys.downloadThreads, 1)
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.english
|
package eu.kanade.tachiyomi.data.source.online.english
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.source.EN
|
import eu.kanade.tachiyomi.data.source.EN
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
|
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||||
|
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
||||||
|
|
||||||
|
class TrackManager(private val context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MYANIMELIST = 1
|
||||||
|
const val ANILIST = 2
|
||||||
|
const val KITSU = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
||||||
|
|
||||||
|
val aniList = Anilist(context, ANILIST)
|
||||||
|
|
||||||
|
val kitsu = Kitsu(context, KITSU)
|
||||||
|
|
||||||
|
val services = listOf(myAnimeList, aniList, kitsu)
|
||||||
|
|
||||||
|
fun getService(id: Int) = services.find { it.id == id }
|
||||||
|
|
||||||
|
fun hasLoggedServices() = services.any { it.isLogged }
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
|
import android.support.annotation.CallSuper
|
||||||
|
import android.support.annotation.DrawableRes
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import rx.Completable
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
abstract class TrackService(val id: Int) {
|
||||||
|
|
||||||
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
|
val networkService: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
|
open val client: OkHttpClient
|
||||||
|
get() = networkService.client
|
||||||
|
|
||||||
|
// Name of the manga sync service to display
|
||||||
|
abstract val name: String
|
||||||
|
|
||||||
|
abstract fun login(username: String, password: String): Completable
|
||||||
|
|
||||||
|
open val isLogged: Boolean
|
||||||
|
get() = !getUsername().isEmpty() &&
|
||||||
|
!getPassword().isEmpty()
|
||||||
|
|
||||||
|
abstract fun add(track: Track): Observable<Track>
|
||||||
|
|
||||||
|
abstract fun update(track: Track): Observable<Track>
|
||||||
|
|
||||||
|
abstract fun bind(track: Track): Observable<Track>
|
||||||
|
|
||||||
|
abstract fun search(query: String): Observable<List<Track>>
|
||||||
|
|
||||||
|
abstract fun refresh(track: Track): Observable<Track>
|
||||||
|
|
||||||
|
abstract fun getStatus(status: Int): String
|
||||||
|
|
||||||
|
abstract fun getStatusList(): List<Int>
|
||||||
|
|
||||||
|
@DrawableRes
|
||||||
|
abstract fun getLogo(): Int
|
||||||
|
|
||||||
|
abstract fun getLogoColor(): Int
|
||||||
|
|
||||||
|
// TODO better support (decimals)
|
||||||
|
abstract fun maxScore(): Int
|
||||||
|
|
||||||
|
abstract fun formatScore(track: Track): String
|
||||||
|
|
||||||
|
fun saveCredentials(username: String, password: String) {
|
||||||
|
preferences.setTrackCredentials(this, username, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
open fun logout() {
|
||||||
|
preferences.setTrackCredentials(this, "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getUsername() = preferences.trackUsername(this)
|
||||||
|
|
||||||
|
fun getPassword() = preferences.trackPassword(this)
|
||||||
|
|
||||||
|
}
|
@ -1,20 +1,20 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class UpdateMangaSyncService : Service() {
|
class TrackUpdateService : Service() {
|
||||||
|
|
||||||
val syncManager: MangaSyncManager by injectLazy()
|
val trackManager: TrackManager by injectLazy()
|
||||||
val db: DatabaseHelper by injectLazy()
|
val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
private lateinit var subscriptions: CompositeSubscription
|
private lateinit var subscriptions: CompositeSubscription
|
||||||
@ -30,9 +30,9 @@ class UpdateMangaSyncService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
val manga = intent.getSerializableExtra(EXTRA_MANGASYNC)
|
val track = intent.getSerializableExtra(EXTRA_TRACK)
|
||||||
if (manga != null) {
|
if (track != null) {
|
||||||
updateLastChapterRead(manga as MangaSync, startId)
|
updateLastChapterRead(track as Track, startId)
|
||||||
return Service.START_REDELIVER_INTENT
|
return Service.START_REDELIVER_INTENT
|
||||||
} else {
|
} else {
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
@ -44,15 +44,15 @@ class UpdateMangaSyncService : Service() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateLastChapterRead(mangaSync: MangaSync, startId: Int) {
|
private fun updateLastChapterRead(track: Track, startId: Int) {
|
||||||
val sync = syncManager.getService(mangaSync.sync_id)
|
val sync = trackManager.getService(track.sync_id)
|
||||||
if (sync == null) {
|
if (sync == null) {
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptions.add(Observable.defer { sync.update(mangaSync) }
|
subscriptions.add(Observable.defer { sync.update(track) }
|
||||||
.flatMap { db.insertMangaSync(mangaSync).asRxObservable() }
|
.flatMap { db.insertTrack(track).asRxObservable() }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({ stopSelf(startId) },
|
.subscribe({ stopSelf(startId) },
|
||||||
@ -61,12 +61,12 @@ class UpdateMangaSyncService : Service() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val EXTRA_MANGASYNC = "extra_mangasync"
|
private val EXTRA_TRACK = "extra_track"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun start(context: Context, mangaSync: MangaSync) {
|
fun start(context: Context, track: Track) {
|
||||||
val intent = Intent(context, UpdateMangaSyncService::class.java)
|
val intent = Intent(context, TrackUpdateService::class.java)
|
||||||
intent.putExtra(EXTRA_MANGASYNC, mangaSync)
|
intent.putExtra(EXTRA_TRACK, track)
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,191 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import com.github.salomonbrys.kotson.int
|
||||||
|
import com.github.salomonbrys.kotson.string
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
import rx.Completable
|
||||||
|
import rx.Observable
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val READING = 1
|
||||||
|
const val COMPLETED = 2
|
||||||
|
const val ON_HOLD = 3
|
||||||
|
const val DROPPED = 4
|
||||||
|
const val PLAN_TO_READ = 5
|
||||||
|
|
||||||
|
const val DEFAULT_STATUS = READING
|
||||||
|
const val DEFAULT_SCORE = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override val name = "AniList"
|
||||||
|
|
||||||
|
private val interceptor by lazy { AnilistInterceptor(getPassword()) }
|
||||||
|
|
||||||
|
private val api by lazy {
|
||||||
|
AnilistApi.createService(networkService.client.newBuilder()
|
||||||
|
.addInterceptor(interceptor)
|
||||||
|
.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLogo() = R.drawable.al
|
||||||
|
|
||||||
|
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
||||||
|
|
||||||
|
override fun maxScore() = 100
|
||||||
|
|
||||||
|
override fun login(username: String, password: String) = login(password)
|
||||||
|
|
||||||
|
fun login(authCode: String): Completable {
|
||||||
|
// Create a new api with the default client to avoid request interceptions.
|
||||||
|
return AnilistApi.createService(client)
|
||||||
|
// Request the access token from the API with the authorization code.
|
||||||
|
.requestAccessToken(authCode)
|
||||||
|
// Save the token in the interceptor.
|
||||||
|
.doOnNext { interceptor.setAuth(it) }
|
||||||
|
// Obtain the authenticated user from the API.
|
||||||
|
.zipWith(api.getCurrentUser().map {
|
||||||
|
preferences.anilistScoreType().set(it["score_type"].int)
|
||||||
|
it["id"].string
|
||||||
|
}, { oauth, user -> Pair(user, oauth.refresh_token!!) })
|
||||||
|
// Save service credentials (username and refresh token).
|
||||||
|
.doOnNext { saveCredentials(it.first, it.second) }
|
||||||
|
// Logout on any error.
|
||||||
|
.doOnError { logout() }
|
||||||
|
.toCompletable()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun logout() {
|
||||||
|
super.logout()
|
||||||
|
interceptor.setAuth(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun search(query: String): Observable<List<Track>> {
|
||||||
|
return api.search(query, 1)
|
||||||
|
.flatMap { Observable.from(it) }
|
||||||
|
.filter { it.type != "Novel" }
|
||||||
|
.map { it.toTrack() }
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getList(): Observable<List<Track>> {
|
||||||
|
return api.getList(getUsername())
|
||||||
|
.flatMap { Observable.from(it.flatten()) }
|
||||||
|
.map { it.toTrack() }
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(track: Track): Observable<Track> {
|
||||||
|
return api.addManga(track.remote_id, track.last_chapter_read, track.getAnilistStatus())
|
||||||
|
.doOnNext { it.body().close() }
|
||||||
|
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
|
||||||
|
.doOnError { Timber.e(it) }
|
||||||
|
.map { track }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(track: Track): Observable<Track> {
|
||||||
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
|
track.status = COMPLETED
|
||||||
|
}
|
||||||
|
return api.updateManga(track.remote_id, track.last_chapter_read, track.getAnilistStatus(),
|
||||||
|
track.getAnilistScore())
|
||||||
|
.doOnNext { it.body().close() }
|
||||||
|
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
|
||||||
|
.doOnError { Timber.e(it) }
|
||||||
|
.map { track }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(track: Track): Observable<Track> {
|
||||||
|
return getList()
|
||||||
|
.flatMap { userlist ->
|
||||||
|
track.sync_id = id
|
||||||
|
val remoteTrack = userlist.find { it.remote_id == track.remote_id }
|
||||||
|
if (remoteTrack != null) {
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
update(track)
|
||||||
|
} else {
|
||||||
|
// Set default fields if it's not found in the list
|
||||||
|
track.score = DEFAULT_SCORE.toFloat()
|
||||||
|
track.status = DEFAULT_STATUS
|
||||||
|
add(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun refresh(track: Track): Observable<Track> {
|
||||||
|
return getList()
|
||||||
|
.map { myList ->
|
||||||
|
val remoteTrack = myList.find { it.remote_id == track.remote_id }
|
||||||
|
if (remoteTrack != null) {
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
|
track
|
||||||
|
} else {
|
||||||
|
throw Exception("Could not find manga")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatusList(): List<Int> {
|
||||||
|
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
|
when (status) {
|
||||||
|
READING -> getString(R.string.reading)
|
||||||
|
COMPLETED -> getString(R.string.completed)
|
||||||
|
ON_HOLD -> getString(R.string.on_hold)
|
||||||
|
DROPPED -> getString(R.string.dropped)
|
||||||
|
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Track.getAnilistStatus() = when (status) {
|
||||||
|
READING -> "reading"
|
||||||
|
COMPLETED -> "completed"
|
||||||
|
ON_HOLD -> "on-hold"
|
||||||
|
DROPPED -> "dropped"
|
||||||
|
PLAN_TO_READ -> "plan to read"
|
||||||
|
else -> throw NotImplementedError("Unknown status")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Track.getAnilistScore(): String = when (preferences.anilistScoreType().getOrDefault()) {
|
||||||
|
// 10 point
|
||||||
|
0 -> Math.floor(score.toDouble() / 10).toInt().toString()
|
||||||
|
// 100 point
|
||||||
|
1 -> score.toInt().toString()
|
||||||
|
// 5 stars
|
||||||
|
2 -> when {
|
||||||
|
score == 0f -> "0"
|
||||||
|
score < 30 -> "1"
|
||||||
|
score < 50 -> "2"
|
||||||
|
score < 70 -> "3"
|
||||||
|
score < 90 -> "4"
|
||||||
|
else -> "5"
|
||||||
|
}
|
||||||
|
// Smiley
|
||||||
|
3 -> when {
|
||||||
|
score == 0f -> "0"
|
||||||
|
score <= 30 -> ":("
|
||||||
|
score <= 60 -> ":|"
|
||||||
|
else -> ":)"
|
||||||
|
}
|
||||||
|
// 10 point decimal
|
||||||
|
4 -> (score / 10).toString()
|
||||||
|
else -> throw Exception("Unknown score type")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun formatScore(track: Track): String {
|
||||||
|
return track.getAnilistScore()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,89 +1,88 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.anilist
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import eu.kanade.tachiyomi.data.mangasync.anilist.model.ALManga
|
import eu.kanade.tachiyomi.data.network.POST
|
||||||
import eu.kanade.tachiyomi.data.mangasync.anilist.model.ALUserLists
|
import eu.kanade.tachiyomi.data.track.anilist.model.ALManga
|
||||||
import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth
|
import eu.kanade.tachiyomi.data.track.anilist.model.ALUserLists
|
||||||
import eu.kanade.tachiyomi.data.network.POST
|
import eu.kanade.tachiyomi.data.track.anilist.model.OAuth
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import retrofit2.http.*
|
import retrofit2.http.*
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
interface AnilistApi {
|
interface AnilistApi {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val clientId = "tachiyomi-hrtje"
|
private const val clientId = "tachiyomi-hrtje"
|
||||||
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
|
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
|
||||||
private const val clientUrl = "tachiyomi://anilist-auth"
|
private const val clientUrl = "tachiyomi://anilist-auth"
|
||||||
private const val baseUrl = "https://anilist.co/api/"
|
private const val baseUrl = "https://anilist.co/api/"
|
||||||
|
|
||||||
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon()
|
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon()
|
||||||
.appendQueryParameter("grant_type", "authorization_code")
|
.appendQueryParameter("grant_type", "authorization_code")
|
||||||
.appendQueryParameter("client_id", clientId)
|
.appendQueryParameter("client_id", clientId)
|
||||||
.appendQueryParameter("redirect_uri", clientUrl)
|
.appendQueryParameter("redirect_uri", clientUrl)
|
||||||
.appendQueryParameter("response_type", "code")
|
.appendQueryParameter("response_type", "code")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token",
|
fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token",
|
||||||
body = FormBody.Builder()
|
body = FormBody.Builder()
|
||||||
.add("grant_type", "refresh_token")
|
.add("grant_type", "refresh_token")
|
||||||
.add("client_id", clientId)
|
.add("client_id", clientId)
|
||||||
.add("client_secret", clientSecret)
|
.add("client_secret", clientSecret)
|
||||||
.add("refresh_token", token)
|
.add("refresh_token", token)
|
||||||
.build())
|
.build())
|
||||||
|
|
||||||
fun createService(client: OkHttpClient) = Retrofit.Builder()
|
fun createService(client: OkHttpClient) = Retrofit.Builder()
|
||||||
.baseUrl(baseUrl)
|
.baseUrl(baseUrl)
|
||||||
.client(client)
|
.client(client)
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||||
.build()
|
.build()
|
||||||
.create(AnilistApi::class.java)
|
.create(AnilistApi::class.java)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("auth/access_token")
|
@POST("auth/access_token")
|
||||||
fun requestAccessToken(
|
fun requestAccessToken(
|
||||||
@Field("code") code: String,
|
@Field("code") code: String,
|
||||||
@Field("grant_type") grant_type: String = "authorization_code",
|
@Field("grant_type") grant_type: String = "authorization_code",
|
||||||
@Field("client_id") client_id: String = clientId,
|
@Field("client_id") client_id: String = clientId,
|
||||||
@Field("client_secret") client_secret: String = clientSecret,
|
@Field("client_secret") client_secret: String = clientSecret,
|
||||||
@Field("redirect_uri") redirect_uri: String = clientUrl)
|
@Field("redirect_uri") redirect_uri: String = clientUrl)
|
||||||
: Observable<OAuth>
|
: Observable<OAuth>
|
||||||
|
|
||||||
@GET("user")
|
@GET("user")
|
||||||
fun getCurrentUser(): Observable<JsonObject>
|
fun getCurrentUser(): Observable<JsonObject>
|
||||||
|
|
||||||
@GET("manga/search/{query}")
|
@GET("manga/search/{query}")
|
||||||
fun search(@Path("query") query: String, @Query("page") page: Int): Observable<List<ALManga>>
|
fun search(@Path("query") query: String, @Query("page") page: Int): Observable<List<ALManga>>
|
||||||
|
|
||||||
@GET("user/{username}/mangalist")
|
@GET("user/{username}/mangalist")
|
||||||
fun getList(@Path("username") username: String): Observable<ALUserLists>
|
fun getList(@Path("username") username: String): Observable<ALUserLists>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@PUT("mangalist")
|
@PUT("mangalist")
|
||||||
fun addManga(
|
fun addManga(
|
||||||
@Field("id") id: Int,
|
@Field("id") id: Int,
|
||||||
@Field("chapters_read") chapters_read: Int,
|
@Field("chapters_read") chapters_read: Int,
|
||||||
@Field("list_status") list_status: String,
|
@Field("list_status") list_status: String)
|
||||||
@Field("score_raw") score_raw: Int)
|
: Observable<Response<ResponseBody>>
|
||||||
: Observable<Response<ResponseBody>>
|
|
||||||
|
@FormUrlEncoded
|
||||||
@FormUrlEncoded
|
@PUT("mangalist")
|
||||||
@PUT("mangalist")
|
fun updateManga(
|
||||||
fun updateManga(
|
@Field("id") id: Int,
|
||||||
@Field("id") id: Int,
|
@Field("chapters_read") chapters_read: Int,
|
||||||
@Field("chapters_read") chapters_read: Int,
|
@Field("list_status") list_status: String,
|
||||||
@Field("list_status") list_status: String,
|
@Field("score") score_raw: String)
|
||||||
@Field("score_raw") score_raw: Int)
|
: Observable<Response<ResponseBody>>
|
||||||
: Observable<Response<ResponseBody>>
|
|
||||||
|
|
||||||
}
|
}
|
@ -1,61 +1,61 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.anilist
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth
|
import eu.kanade.tachiyomi.data.track.anilist.model.OAuth
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
|
||||||
class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OAuth object used for authenticated requests.
|
* OAuth object used for authenticated requests.
|
||||||
*
|
*
|
||||||
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
|
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
|
||||||
* before its original expiration date.
|
* before its original expiration date.
|
||||||
*/
|
*/
|
||||||
private var oauth: OAuth? = null
|
private var oauth: OAuth? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
|
field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
if (refreshToken.isNullOrEmpty()) {
|
if (refreshToken.isNullOrEmpty()) {
|
||||||
throw Exception("Not authenticated with Anilist")
|
throw Exception("Not authenticated with Anilist")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh access token if null or expired.
|
// Refresh access token if null or expired.
|
||||||
if (oauth == null || oauth!!.isExpired()) {
|
if (oauth == null || oauth!!.isExpired()) {
|
||||||
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
|
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
|
||||||
oauth = if (response.isSuccessful) {
|
oauth = if (response.isSuccessful) {
|
||||||
Gson().fromJson(response.body().string(), OAuth::class.java)
|
Gson().fromJson(response.body().string(), OAuth::class.java)
|
||||||
} else {
|
} else {
|
||||||
response.close()
|
response.close()
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throw on null auth.
|
// Throw on null auth.
|
||||||
if (oauth == null) {
|
if (oauth == null) {
|
||||||
throw Exception("Access token wasn't refreshed")
|
throw Exception("Access token wasn't refreshed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the authorization header to the original request.
|
// Add the authorization header to the original request.
|
||||||
val authRequest = originalRequest.newBuilder()
|
val authRequest = originalRequest.newBuilder()
|
||||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return chain.proceed(authRequest)
|
return chain.proceed(authRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the user authenticates with Anilist for the first time. Sets the refresh token
|
* Called when the user authenticates with Anilist for the first time. Sets the refresh token
|
||||||
* and the oauth object.
|
* and the oauth object.
|
||||||
*/
|
*/
|
||||||
fun setAuth(oauth: OAuth?) {
|
fun setAuth(oauth: OAuth?) {
|
||||||
refreshToken = oauth?.refresh_token
|
refreshToken = oauth?.refresh_token
|
||||||
this.oauth = oauth
|
this.oauth = oauth
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,17 +1,17 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
package eu.kanade.tachiyomi.data.track.anilist.model
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
|
||||||
data class ALManga(
|
data class ALManga(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val title_romaji: String,
|
val title_romaji: String,
|
||||||
val type: String,
|
val type: String,
|
||||||
val total_chapters: Int) {
|
val total_chapters: Int) {
|
||||||
|
|
||||||
fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply {
|
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
||||||
remote_id = this@ALManga.id
|
remote_id = this@ALManga.id
|
||||||
title = title_romaji
|
title = title_romaji
|
||||||
total_chapters = this@ALManga.total_chapters
|
total_chapters = this@ALManga.total_chapters
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
package eu.kanade.tachiyomi.data.track.anilist.model
|
||||||
|
|
||||||
data class ALUserLists(val lists: Map<String, List<ALUserManga>>) {
|
data class ALUserLists(val lists: Map<String, List<ALUserManga>>) {
|
||||||
|
|
||||||
fun flatten() = lists.values.flatten()
|
fun flatten() = lists.values.flatten()
|
||||||
}
|
}
|
@ -1,29 +1,29 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
package eu.kanade.tachiyomi.data.track.anilist.model
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
|
|
||||||
data class ALUserManga(
|
data class ALUserManga(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val list_status: String,
|
val list_status: String,
|
||||||
val score_raw: Int,
|
val score_raw: Int,
|
||||||
val chapters_read: Int,
|
val chapters_read: Int,
|
||||||
val manga: ALManga) {
|
val manga: ALManga) {
|
||||||
|
|
||||||
fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply {
|
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
||||||
remote_id = manga.id
|
remote_id = manga.id
|
||||||
status = getMangaSyncStatus()
|
status = toTrackStatus()
|
||||||
score = score_raw.toFloat()
|
score = score_raw.toFloat()
|
||||||
last_chapter_read = chapters_read
|
last_chapter_read = chapters_read
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMangaSyncStatus() = when (list_status) {
|
fun toTrackStatus() = when (list_status) {
|
||||||
"reading" -> Anilist.READING
|
"reading" -> Anilist.READING
|
||||||
"completed" -> Anilist.COMPLETED
|
"completed" -> Anilist.COMPLETED
|
||||||
"on-hold" -> Anilist.ON_HOLD
|
"on-hold" -> Anilist.ON_HOLD
|
||||||
"dropped" -> Anilist.DROPPED
|
"dropped" -> Anilist.DROPPED
|
||||||
"plan to read" -> Anilist.PLAN_TO_READ
|
"plan to read" -> Anilist.PLAN_TO_READ
|
||||||
else -> throw NotImplementedError("Unknown status")
|
else -> throw NotImplementedError("Unknown status")
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,11 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
package eu.kanade.tachiyomi.data.track.anilist.model
|
||||||
|
|
||||||
data class OAuth(
|
data class OAuth(
|
||||||
val access_token: String,
|
val access_token: String,
|
||||||
val token_type: String,
|
val token_type: String,
|
||||||
val expires: Long,
|
val expires: Long,
|
||||||
val expires_in: Long,
|
val expires_in: Long,
|
||||||
val refresh_token: String?) {
|
val refresh_token: String?) {
|
||||||
|
|
||||||
fun isExpired() = System.currentTimeMillis() > expires
|
fun isExpired() = System.currentTimeMillis() > expires
|
||||||
}
|
}
|
219
app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt
Normal file
219
app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import eu.kanade.tachiyomi.data.network.POST
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import retrofit2.http.*
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
interface KitsuApi {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val clientId = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
||||||
|
private const val clientSecret = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
||||||
|
private const val baseUrl = "https://kitsu.io/api/edge/"
|
||||||
|
private const val loginUrl = "https://kitsu.io/api/"
|
||||||
|
|
||||||
|
fun createService(client: OkHttpClient) = Retrofit.Builder()
|
||||||
|
.baseUrl(baseUrl)
|
||||||
|
.client(client)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||||
|
.build()
|
||||||
|
.create(KitsuApi::class.java)
|
||||||
|
|
||||||
|
fun createLoginService(client: OkHttpClient) = Retrofit.Builder()
|
||||||
|
.baseUrl(loginUrl)
|
||||||
|
.client(client)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||||
|
.build()
|
||||||
|
.create(KitsuApi::class.java)
|
||||||
|
|
||||||
|
fun refreshTokenRequest(token: String) = POST("${loginUrl}oauth/token",
|
||||||
|
body = FormBody.Builder()
|
||||||
|
.add("grant_type", "refresh_token")
|
||||||
|
.add("client_id", clientId)
|
||||||
|
.add("client_secret", clientSecret)
|
||||||
|
.add("refresh_token", token)
|
||||||
|
.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST("oauth/token")
|
||||||
|
fun requestAccessToken(
|
||||||
|
@Field("username") username: String,
|
||||||
|
@Field("password") password: String,
|
||||||
|
@Field("grant_type") grantType: String = "password",
|
||||||
|
@Field("client_id") client_id: String = clientId,
|
||||||
|
@Field("client_secret") client_secret: String = clientSecret
|
||||||
|
) : Observable<OAuth>
|
||||||
|
|
||||||
|
@GET("users")
|
||||||
|
fun getCurrentUser(
|
||||||
|
@Query("filter[self]", encoded = true) self: Boolean = true
|
||||||
|
) : Observable<JsonObject>
|
||||||
|
|
||||||
|
@GET("manga")
|
||||||
|
fun search(
|
||||||
|
@Query("filter[text]", encoded = true) query: String
|
||||||
|
): Observable<JsonObject>
|
||||||
|
|
||||||
|
@GET("library-entries")
|
||||||
|
fun getLibManga(
|
||||||
|
@Query("filter[id]", encoded = true) remoteId: Int,
|
||||||
|
@Query("include") includes: String = "media"
|
||||||
|
) : Observable<JsonObject>
|
||||||
|
|
||||||
|
@GET("library-entries")
|
||||||
|
fun findLibManga(
|
||||||
|
@Query("filter[user_id]", encoded = true) userId: String,
|
||||||
|
@Query("filter[media_id]", encoded = true) remoteId: Int,
|
||||||
|
@Query("page[limit]", encoded = true) limit: Int = 10000,
|
||||||
|
@Query("include") includes: String = "media"
|
||||||
|
) : Observable<JsonObject>
|
||||||
|
|
||||||
|
@Headers("Content-Type: application/vnd.api+json")
|
||||||
|
@POST("library-entries")
|
||||||
|
fun addLibManga(
|
||||||
|
@Body data: JsonObject
|
||||||
|
) : Observable<JsonObject>
|
||||||
|
|
||||||
|
@Headers("Content-Type: application/vnd.api+json")
|
||||||
|
@PATCH("library-entries/{id}")
|
||||||
|
fun updateLibManga(
|
||||||
|
@Path("id") remoteId: Int,
|
||||||
|
@Body data: JsonObject
|
||||||
|
) : Observable<JsonObject>
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAuth object used for authenticated requests.
|
||||||
|
*/
|
||||||
|
private var oauth: OAuth? = kitsu.restoreToken()
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
|
val currAuth = oauth ?: throw Exception("Not authenticated with Kitsu")
|
||||||
|
|
||||||
|
val refreshToken = currAuth.refresh_token!!
|
||||||
|
|
||||||
|
// Refresh access token if expired.
|
||||||
|
if (currAuth.isExpired()) {
|
||||||
|
val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken))
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
newAuth(gson.fromJson(response.body().string(), OAuth::class.java))
|
||||||
|
} else {
|
||||||
|
response.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the authorization header to the original request.
|
||||||
|
val authRequest = originalRequest.newBuilder()
|
||||||
|
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||||
|
.header("Accept", "application/vnd.api+json")
|
||||||
|
.header("Content-Type", "application/vnd.api+json")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return chain.proceed(authRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun newAuth(oauth: OAuth?) {
|
||||||
|
this.oauth = oauth
|
||||||
|
kitsu.saveToken(oauth)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
|
import android.support.annotation.CallSuper
|
||||||
|
import com.github.salomonbrys.kotson.*
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
|
||||||
|
open class KitsuManga(obj: JsonObject) {
|
||||||
|
val id by obj.byInt
|
||||||
|
val canonicalTitle by obj["attributes"].byString
|
||||||
|
val chapterCount = obj["attributes"]["chapterCount"].nullInt
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
open fun toTrack() = Track.create(TrackManager.KITSU).apply {
|
||||||
|
remote_id = this@KitsuManga.id
|
||||||
|
title = canonicalTitle
|
||||||
|
total_chapters = chapterCount ?: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class KitsuLibManga(obj: JsonObject, manga: JsonObject) : KitsuManga(manga) {
|
||||||
|
val remoteId by obj.byInt("id")
|
||||||
|
val status by obj["attributes"].byString
|
||||||
|
val rating = obj["attributes"]["rating"].nullString
|
||||||
|
val progress by obj["attributes"].byInt
|
||||||
|
|
||||||
|
override fun toTrack() = super.toTrack().apply {
|
||||||
|
remote_id = remoteId
|
||||||
|
status = toTrackStatus()
|
||||||
|
score = rating?.let { it.toFloat() * 2 } ?: 0f
|
||||||
|
last_chapter_read = progress
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toTrackStatus() = when (status) {
|
||||||
|
"current" -> Kitsu.READING
|
||||||
|
"completed" -> Kitsu.COMPLETED
|
||||||
|
"on_hold" -> Kitsu.ON_HOLD
|
||||||
|
"dropped" -> Kitsu.DROPPED
|
||||||
|
"planned" -> Kitsu.PLAN_TO_READ
|
||||||
|
else -> throw Exception("Unknown status")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
|
data class OAuth(
|
||||||
|
val access_token: String,
|
||||||
|
val token_type: String,
|
||||||
|
val created_at: Long,
|
||||||
|
val expires_in: Long,
|
||||||
|
val refresh_token: String?) {
|
||||||
|
|
||||||
|
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
|
||||||
|
}
|
@ -1,222 +1,263 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.myanimelist
|
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.graphics.Color
|
||||||
import android.util.Xml
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.R
|
import android.util.Xml
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.network.GET
|
import eu.kanade.tachiyomi.data.network.GET
|
||||||
import eu.kanade.tachiyomi.data.network.POST
|
import eu.kanade.tachiyomi.data.network.POST
|
||||||
import eu.kanade.tachiyomi.data.network.asObservable
|
import eu.kanade.tachiyomi.data.network.asObservable
|
||||||
import eu.kanade.tachiyomi.util.selectInt
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.util.selectText
|
import eu.kanade.tachiyomi.util.selectInt
|
||||||
import okhttp3.Credentials
|
import eu.kanade.tachiyomi.util.selectText
|
||||||
import okhttp3.FormBody
|
import okhttp3.Credentials
|
||||||
import okhttp3.Headers
|
import okhttp3.FormBody
|
||||||
import okhttp3.RequestBody
|
import okhttp3.Headers
|
||||||
import org.jsoup.Jsoup
|
import okhttp3.RequestBody
|
||||||
import org.xmlpull.v1.XmlSerializer
|
import org.jsoup.Jsoup
|
||||||
import rx.Completable
|
import org.xmlpull.v1.XmlSerializer
|
||||||
import rx.Observable
|
import rx.Completable
|
||||||
import java.io.StringWriter
|
import rx.Observable
|
||||||
|
import java.io.StringWriter
|
||||||
class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) {
|
|
||||||
|
class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
||||||
private lateinit var headers: Headers
|
|
||||||
|
private lateinit var headers: Headers
|
||||||
companion object {
|
|
||||||
val BASE_URL = "https://myanimelist.net"
|
companion object {
|
||||||
|
const val BASE_URL = "https://myanimelist.net"
|
||||||
private val ENTRY_TAG = "entry"
|
|
||||||
private val CHAPTER_TAG = "chapter"
|
private val ENTRY_TAG = "entry"
|
||||||
private val SCORE_TAG = "score"
|
private val CHAPTER_TAG = "chapter"
|
||||||
private val STATUS_TAG = "status"
|
private val SCORE_TAG = "score"
|
||||||
|
private val STATUS_TAG = "status"
|
||||||
val READING = 1
|
|
||||||
val COMPLETED = 2
|
const val READING = 1
|
||||||
val ON_HOLD = 3
|
const val COMPLETED = 2
|
||||||
val DROPPED = 4
|
const val ON_HOLD = 3
|
||||||
val PLAN_TO_READ = 6
|
const val DROPPED = 4
|
||||||
|
const val PLAN_TO_READ = 6
|
||||||
val DEFAULT_STATUS = READING
|
|
||||||
val DEFAULT_SCORE = 0
|
const val DEFAULT_STATUS = READING
|
||||||
}
|
const val DEFAULT_SCORE = 0
|
||||||
|
|
||||||
init {
|
const val PREFIX_MY = "my:"
|
||||||
val username = getUsername()
|
}
|
||||||
val password = getPassword()
|
|
||||||
|
init {
|
||||||
if (!username.isEmpty() && !password.isEmpty()) {
|
val username = getUsername()
|
||||||
createHeaders(username, password)
|
val password = getPassword()
|
||||||
}
|
|
||||||
}
|
if (!username.isEmpty() && !password.isEmpty()) {
|
||||||
|
createHeaders(username, password)
|
||||||
override val name: String
|
}
|
||||||
get() = "MyAnimeList"
|
}
|
||||||
|
|
||||||
fun getLoginUrl() = Uri.parse(BASE_URL).buildUpon()
|
override val name: String
|
||||||
.appendEncodedPath("api/account/verify_credentials.xml")
|
get() = "MyAnimeList"
|
||||||
.toString()
|
|
||||||
|
override fun getLogo() = R.drawable.mal
|
||||||
fun getSearchUrl(query: String) = Uri.parse(BASE_URL).buildUpon()
|
|
||||||
.appendEncodedPath("api/manga/search.xml")
|
override fun getLogoColor() = Color.rgb(46, 81, 162)
|
||||||
.appendQueryParameter("q", query)
|
|
||||||
.toString()
|
override fun maxScore() = 10
|
||||||
|
|
||||||
fun getListUrl(username: String) = Uri.parse(BASE_URL).buildUpon()
|
override fun formatScore(track: Track): String {
|
||||||
.appendPath("malappinfo.php")
|
return track.score.toInt().toString()
|
||||||
.appendQueryParameter("u", username)
|
}
|
||||||
.appendQueryParameter("status", "all")
|
|
||||||
.appendQueryParameter("type", "manga")
|
fun getLoginUrl() = Uri.parse(BASE_URL).buildUpon()
|
||||||
.toString()
|
.appendEncodedPath("api/account/verify_credentials.xml")
|
||||||
|
.toString()
|
||||||
fun getUpdateUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
|
|
||||||
.appendEncodedPath("api/mangalist/update")
|
fun getSearchUrl(query: String) = Uri.parse(BASE_URL).buildUpon()
|
||||||
.appendPath("${manga.remote_id}.xml")
|
.appendEncodedPath("api/manga/search.xml")
|
||||||
.toString()
|
.appendQueryParameter("q", query)
|
||||||
|
.toString()
|
||||||
fun getAddUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
|
|
||||||
.appendEncodedPath("api/mangalist/add")
|
fun getListUrl(username: String) = Uri.parse(BASE_URL).buildUpon()
|
||||||
.appendPath("${manga.remote_id}.xml")
|
.appendPath("malappinfo.php")
|
||||||
.toString()
|
.appendQueryParameter("u", username)
|
||||||
|
.appendQueryParameter("status", "all")
|
||||||
override fun login(username: String, password: String): Completable {
|
.appendQueryParameter("type", "manga")
|
||||||
createHeaders(username, password)
|
.toString()
|
||||||
return client.newCall(GET(getLoginUrl(), headers))
|
|
||||||
.asObservable()
|
fun getUpdateUrl(track: Track) = Uri.parse(BASE_URL).buildUpon()
|
||||||
.doOnNext { it.close() }
|
.appendEncodedPath("api/mangalist/update")
|
||||||
.doOnNext { if (it.code() != 200) throw Exception("Login error") }
|
.appendPath("${track.remote_id}.xml")
|
||||||
.toCompletable()
|
.toString()
|
||||||
}
|
|
||||||
|
fun getAddUrl(track: Track) = Uri.parse(BASE_URL).buildUpon()
|
||||||
fun search(query: String): Observable<List<MangaSync>> {
|
.appendEncodedPath("api/mangalist/add")
|
||||||
return client.newCall(GET(getSearchUrl(query), headers))
|
.appendPath("${track.remote_id}.xml")
|
||||||
.asObservable()
|
.toString()
|
||||||
.map { Jsoup.parse(it.body().string()) }
|
|
||||||
.flatMap { Observable.from(it.select("entry")) }
|
override fun login(username: String, password: String): Completable {
|
||||||
.filter { it.select("type").text() != "Novel" }
|
createHeaders(username, password)
|
||||||
.map {
|
return client.newCall(GET(getLoginUrl(), headers))
|
||||||
MangaSync.create(id).apply {
|
.asObservable()
|
||||||
title = it.selectText("title")!!
|
.doOnNext { it.close() }
|
||||||
remote_id = it.selectInt("id")
|
.doOnNext { if (it.code() != 200) throw Exception("Login error") }
|
||||||
total_chapters = it.selectInt("chapters")
|
.doOnNext { saveCredentials(username, password) }
|
||||||
}
|
.doOnError { logout() }
|
||||||
}
|
.toCompletable()
|
||||||
.toList()
|
}
|
||||||
}
|
|
||||||
|
override fun search(query: String): Observable<List<Track>> {
|
||||||
// MAL doesn't support score with decimals
|
return if (query.startsWith(PREFIX_MY)) {
|
||||||
fun getList(): Observable<List<MangaSync>> {
|
val realQuery = query.substring(PREFIX_MY.length).toLowerCase().trim()
|
||||||
return networkService.forceCacheClient
|
getList()
|
||||||
.newCall(GET(getListUrl(getUsername()), headers))
|
.flatMap { Observable.from(it) }
|
||||||
.asObservable()
|
.filter { realQuery in it.title.toLowerCase() }
|
||||||
.map { Jsoup.parse(it.body().string()) }
|
.toList()
|
||||||
.flatMap { Observable.from(it.select("manga")) }
|
} else {
|
||||||
.map {
|
client.newCall(GET(getSearchUrl(query), headers))
|
||||||
MangaSync.create(id).apply {
|
.asObservable()
|
||||||
title = it.selectText("series_title")!!
|
.map { Jsoup.parse(it.body().string()) }
|
||||||
remote_id = it.selectInt("series_mangadb_id")
|
.flatMap { Observable.from(it.select("entry")) }
|
||||||
last_chapter_read = it.selectInt("my_read_chapters")
|
.filter { it.select("type").text() != "Novel" }
|
||||||
status = it.selectInt("my_status")
|
.map {
|
||||||
score = it.selectInt("my_score").toFloat()
|
Track.create(id).apply {
|
||||||
total_chapters = it.selectInt("series_chapters")
|
title = it.selectText("title")!!
|
||||||
}
|
remote_id = it.selectInt("id")
|
||||||
}
|
total_chapters = it.selectInt("chapters")
|
||||||
.toList()
|
}
|
||||||
}
|
}
|
||||||
|
.toList()
|
||||||
override fun update(manga: MangaSync): Observable<MangaSync> {
|
}
|
||||||
return Observable.defer {
|
}
|
||||||
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
|
|
||||||
manga.status = COMPLETED
|
override fun refresh(track: Track): Observable<Track> {
|
||||||
}
|
return getList()
|
||||||
client.newCall(POST(getUpdateUrl(manga), headers, getMangaPostPayload(manga)))
|
.map { myList ->
|
||||||
.asObservable()
|
val remoteTrack = myList.find { it.remote_id == track.remote_id }
|
||||||
.doOnNext { it.close() }
|
if (remoteTrack != null) {
|
||||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
|
track.copyPersonalFrom(remoteTrack)
|
||||||
.map { manga }
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
}
|
track
|
||||||
|
} else {
|
||||||
}
|
throw Exception("Could not find manga")
|
||||||
|
}
|
||||||
override fun add(manga: MangaSync): Observable<MangaSync> {
|
}
|
||||||
return Observable.defer {
|
}
|
||||||
client.newCall(POST(getAddUrl(manga), headers, getMangaPostPayload(manga)))
|
|
||||||
.asObservable()
|
// MAL doesn't support score with decimals
|
||||||
.doOnNext { it.close() }
|
fun getList(): Observable<List<Track>> {
|
||||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
|
return networkService.forceCacheClient
|
||||||
.map { manga }
|
.newCall(GET(getListUrl(getUsername()), headers))
|
||||||
}
|
.asObservable()
|
||||||
}
|
.map { Jsoup.parse(it.body().string()) }
|
||||||
|
.flatMap { Observable.from(it.select("manga")) }
|
||||||
private fun getMangaPostPayload(manga: MangaSync): RequestBody {
|
.map {
|
||||||
val xml = Xml.newSerializer()
|
Track.create(id).apply {
|
||||||
val writer = StringWriter()
|
title = it.selectText("series_title")!!
|
||||||
|
remote_id = it.selectInt("series_mangadb_id")
|
||||||
with(xml) {
|
last_chapter_read = it.selectInt("my_read_chapters")
|
||||||
setOutput(writer)
|
status = it.selectInt("my_status")
|
||||||
startDocument("UTF-8", false)
|
score = it.selectInt("my_score").toFloat()
|
||||||
startTag("", ENTRY_TAG)
|
total_chapters = it.selectInt("series_chapters")
|
||||||
|
}
|
||||||
// Last chapter read
|
}
|
||||||
if (manga.last_chapter_read != 0) {
|
.toList()
|
||||||
inTag(CHAPTER_TAG, manga.last_chapter_read.toString())
|
}
|
||||||
}
|
|
||||||
// Manga status in the list
|
override fun update(track: Track): Observable<Track> {
|
||||||
inTag(STATUS_TAG, manga.status.toString())
|
return Observable.defer {
|
||||||
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
// Manga score
|
track.status = COMPLETED
|
||||||
inTag(SCORE_TAG, manga.score.toString())
|
}
|
||||||
|
client.newCall(POST(getUpdateUrl(track), headers, getMangaPostPayload(track)))
|
||||||
endTag("", ENTRY_TAG)
|
.asObservable()
|
||||||
endDocument()
|
.doOnNext { it.close() }
|
||||||
}
|
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
|
||||||
|
.map { track }
|
||||||
val form = FormBody.Builder()
|
}
|
||||||
form.add("data", writer.toString())
|
|
||||||
return form.build()
|
}
|
||||||
}
|
|
||||||
|
override fun add(track: Track): Observable<Track> {
|
||||||
fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
|
return Observable.defer {
|
||||||
startTag(namespace, tag)
|
client.newCall(POST(getAddUrl(track), headers, getMangaPostPayload(track)))
|
||||||
text(body)
|
.asObservable()
|
||||||
endTag(namespace, tag)
|
.doOnNext { it.close() }
|
||||||
}
|
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
|
||||||
|
.map { track }
|
||||||
override fun bind(manga: MangaSync): Observable<MangaSync> {
|
}
|
||||||
return getList()
|
}
|
||||||
.flatMap { userlist ->
|
|
||||||
manga.sync_id = id
|
private fun getMangaPostPayload(track: Track): RequestBody {
|
||||||
val mangaFromList = userlist.find { it.remote_id == manga.remote_id }
|
val xml = Xml.newSerializer()
|
||||||
if (mangaFromList != null) {
|
val writer = StringWriter()
|
||||||
manga.copyPersonalFrom(mangaFromList)
|
|
||||||
update(manga)
|
with(xml) {
|
||||||
} else {
|
setOutput(writer)
|
||||||
// Set default fields if it's not found in the list
|
startDocument("UTF-8", false)
|
||||||
manga.score = DEFAULT_SCORE.toFloat()
|
startTag("", ENTRY_TAG)
|
||||||
manga.status = DEFAULT_STATUS
|
|
||||||
add(manga)
|
// Last chapter read
|
||||||
}
|
if (track.last_chapter_read != 0) {
|
||||||
}
|
inTag(CHAPTER_TAG, track.last_chapter_read.toString())
|
||||||
}
|
}
|
||||||
|
// Manga status in the list
|
||||||
override fun getStatus(status: Int): String = with(context) {
|
inTag(STATUS_TAG, track.status.toString())
|
||||||
when (status) {
|
|
||||||
READING -> getString(R.string.reading)
|
// Manga score
|
||||||
COMPLETED -> getString(R.string.completed)
|
inTag(SCORE_TAG, track.score.toString())
|
||||||
ON_HOLD -> getString(R.string.on_hold)
|
|
||||||
DROPPED -> getString(R.string.dropped)
|
endTag("", ENTRY_TAG)
|
||||||
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
endDocument()
|
||||||
else -> ""
|
}
|
||||||
}
|
|
||||||
}
|
val form = FormBody.Builder()
|
||||||
|
form.add("data", writer.toString())
|
||||||
fun createHeaders(username: String, password: String) {
|
return form.build()
|
||||||
val builder = Headers.Builder()
|
}
|
||||||
builder.add("Authorization", Credentials.basic(username, password))
|
|
||||||
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
|
fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
|
||||||
headers = builder.build()
|
startTag(namespace, tag)
|
||||||
}
|
text(body)
|
||||||
|
endTag(namespace, tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun bind(track: Track): Observable<Track> {
|
||||||
|
return getList()
|
||||||
|
.flatMap { userlist ->
|
||||||
|
track.sync_id = id
|
||||||
|
val remoteTrack = userlist.find { it.remote_id == track.remote_id }
|
||||||
|
if (remoteTrack != null) {
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
update(track)
|
||||||
|
} else {
|
||||||
|
// Set default fields if it's not found in the list
|
||||||
|
track.score = DEFAULT_SCORE.toFloat()
|
||||||
|
track.status = DEFAULT_STATUS
|
||||||
|
add(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
|
when (status) {
|
||||||
|
READING -> getString(R.string.reading)
|
||||||
|
COMPLETED -> getString(R.string.completed)
|
||||||
|
ON_HOLD -> getString(R.string.on_hold)
|
||||||
|
DROPPED -> getString(R.string.dropped)
|
||||||
|
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatusList(): List<Int> {
|
||||||
|
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createHeaders(username: String, password: String) {
|
||||||
|
val builder = Headers.Builder()
|
||||||
|
builder.add("Authorization", Credentials.basic(username, password))
|
||||||
|
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
|
||||||
|
headers = builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -3,15 +3,18 @@ package eu.kanade.tachiyomi.ui.manga
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.support.graphics.drawable.VectorDrawableCompat
|
||||||
import android.support.v4.app.Fragment
|
import android.support.v4.app.Fragment
|
||||||
import android.support.v4.app.FragmentManager
|
import android.support.v4.app.FragmentManager
|
||||||
import android.support.v4.app.FragmentPagerAdapter
|
import android.support.v4.app.FragmentPagerAdapter
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
||||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment
|
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment
|
||||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoFragment
|
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoFragment
|
||||||
import eu.kanade.tachiyomi.ui.manga.myanimelist.MyAnimeListFragment
|
import eu.kanade.tachiyomi.ui.manga.track.TrackFragment
|
||||||
import eu.kanade.tachiyomi.util.SharedData
|
import eu.kanade.tachiyomi.util.SharedData
|
||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.toast
|
||||||
import kotlinx.android.synthetic.main.activity_manga.*
|
import kotlinx.android.synthetic.main.activity_manga.*
|
||||||
@ -28,7 +31,7 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
|
|||||||
const val FROM_LAUNCHER_EXTRA = "from_launcher"
|
const val FROM_LAUNCHER_EXTRA = "from_launcher"
|
||||||
const val INFO_FRAGMENT = 0
|
const val INFO_FRAGMENT = 0
|
||||||
const val CHAPTERS_FRAGMENT = 1
|
const val CHAPTERS_FRAGMENT = 1
|
||||||
const val MYANIMELIST_FRAGMENT = 2
|
const val TRACK_FRAGMENT = 2
|
||||||
|
|
||||||
fun newIntent(context: Context, manga: Manga, fromCatalogue: Boolean = false): Intent {
|
fun newIntent(context: Context, manga: Manga, fromCatalogue: Boolean = false): Intent {
|
||||||
SharedData.put(MangaEvent(manga))
|
SharedData.put(MangaEvent(manga))
|
||||||
@ -71,6 +74,7 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
|
|||||||
fromCatalogue = intent.getBooleanExtra(FROM_CATALOGUE_EXTRA, false)
|
fromCatalogue = intent.getBooleanExtra(FROM_CATALOGUE_EXTRA, false)
|
||||||
|
|
||||||
adapter = MangaDetailAdapter(supportFragmentManager, this)
|
adapter = MangaDetailAdapter(supportFragmentManager, this)
|
||||||
|
view_pager.offscreenPageLimit = 3
|
||||||
view_pager.adapter = adapter
|
view_pager.adapter = adapter
|
||||||
|
|
||||||
tabs.setupWithViewPager(view_pager)
|
tabs.setupWithViewPager(view_pager)
|
||||||
@ -85,33 +89,50 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
|
|||||||
setToolbarTitle(manga.title)
|
setToolbarTitle(manga.title)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class MangaDetailAdapter(fm: FragmentManager, activity: MangaActivity) : FragmentPagerAdapter(fm) {
|
fun setTrackingIcon(visible: Boolean) {
|
||||||
|
val tab = tabs.getTabAt(TRACK_FRAGMENT) ?: return
|
||||||
|
val drawable = if (visible)
|
||||||
|
VectorDrawableCompat.create(resources, R.drawable.ic_done_white_18dp, null)
|
||||||
|
else null
|
||||||
|
|
||||||
private var pageCount: Int = 0
|
// I had no choice but to use reflection...
|
||||||
private val tabTitles = arrayOf(activity.getString(R.string.manga_detail_tab),
|
val field = tab.javaClass.getDeclaredField("mView").apply { isAccessible = true }
|
||||||
activity.getString(R.string.manga_chapters_tab), "MAL")
|
val view = field.get(tab) as LinearLayout
|
||||||
|
val textView = view.getChildAt(1) as TextView
|
||||||
|
textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null)
|
||||||
|
textView.compoundDrawablePadding = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MangaDetailAdapter(fm: FragmentManager, activity: MangaActivity)
|
||||||
|
: FragmentPagerAdapter(fm) {
|
||||||
|
|
||||||
|
private var tabCount = 2
|
||||||
|
|
||||||
|
private val tabTitles = listOf(
|
||||||
|
R.string.manga_detail_tab,
|
||||||
|
R.string.manga_chapters_tab,
|
||||||
|
R.string.manga_tracking_tab)
|
||||||
|
.map { activity.getString(it) }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
pageCount = 2
|
if (!activity.fromCatalogue && activity.presenter.trackManager.hasLoggedServices())
|
||||||
if (!activity.fromCatalogue && activity.presenter.syncManager.myAnimeList.isLogged)
|
tabCount++
|
||||||
pageCount++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCount(): Int {
|
override fun getCount(): Int {
|
||||||
return pageCount
|
return tabCount
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItem(position: Int): Fragment? {
|
override fun getItem(position: Int): Fragment {
|
||||||
when (position) {
|
when (position) {
|
||||||
INFO_FRAGMENT -> return MangaInfoFragment.newInstance()
|
INFO_FRAGMENT -> return MangaInfoFragment.newInstance()
|
||||||
CHAPTERS_FRAGMENT -> return ChaptersFragment.newInstance()
|
CHAPTERS_FRAGMENT -> return ChaptersFragment.newInstance()
|
||||||
MYANIMELIST_FRAGMENT -> return MyAnimeListFragment.newInstance()
|
TRACK_FRAGMENT -> return TrackFragment.newInstance()
|
||||||
else -> return null
|
else -> throw Exception("Unknown position")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPageTitle(position: Int): CharSequence {
|
override fun getPageTitle(position: Int): CharSequence {
|
||||||
// Generate title based on item position
|
|
||||||
return tabTitles[position]
|
return tabTitles[position]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.manga
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
|
import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
|
||||||
import eu.kanade.tachiyomi.util.SharedData
|
import eu.kanade.tachiyomi.util.SharedData
|
||||||
@ -22,9 +22,9 @@ class MangaPresenter : BasePresenter<MangaActivity>() {
|
|||||||
val db: DatabaseHelper by injectLazy()
|
val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manga sync manager.
|
* Tracking manager.
|
||||||
*/
|
*/
|
||||||
val syncManager: MangaSyncManager by injectLazy()
|
val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manga associated with this instance.
|
* Manga associated with this instance.
|
||||||
|
@ -1,124 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.myanimelist
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.support.v4.app.DialogFragment
|
|
||||||
import android.view.View
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
|
|
||||||
import kotlinx.android.synthetic.main.dialog_myanimelist_search.view.*
|
|
||||||
import rx.Subscription
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.subjects.PublishSubject
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class MyAnimeListDialogFragment : DialogFragment() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun newInstance(): MyAnimeListDialogFragment {
|
|
||||||
return MyAnimeListDialogFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var v: View
|
|
||||||
|
|
||||||
lateinit var adapter: MyAnimeListSearchAdapter
|
|
||||||
private set
|
|
||||||
|
|
||||||
lateinit var querySubject: PublishSubject<String>
|
|
||||||
private set
|
|
||||||
|
|
||||||
private var selectedItem: MangaSync? = null
|
|
||||||
|
|
||||||
private var searchSubscription: Subscription? = null
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedState: Bundle?): Dialog {
|
|
||||||
val dialog = MaterialDialog.Builder(activity)
|
|
||||||
.customView(R.layout.dialog_myanimelist_search, false)
|
|
||||||
.positiveText(android.R.string.ok)
|
|
||||||
.negativeText(android.R.string.cancel)
|
|
||||||
.onPositive { dialog1, which -> onPositiveButtonClick() }
|
|
||||||
.build()
|
|
||||||
|
|
||||||
onViewCreated(dialog.view, savedState)
|
|
||||||
|
|
||||||
return dialog
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
|
||||||
v = view
|
|
||||||
|
|
||||||
// Create adapter
|
|
||||||
adapter = MyAnimeListSearchAdapter(activity)
|
|
||||||
view.myanimelist_search_results.adapter = adapter
|
|
||||||
|
|
||||||
// Set listeners
|
|
||||||
view.myanimelist_search_results.setOnItemClickListener { parent, viewList, position, id ->
|
|
||||||
selectedItem = adapter.getItem(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do an initial search based on the manga's title
|
|
||||||
if (savedState == null) {
|
|
||||||
val title = presenter.manga.title
|
|
||||||
view.myanimelist_search_field.append(title)
|
|
||||||
search(title)
|
|
||||||
}
|
|
||||||
|
|
||||||
querySubject = PublishSubject.create<String>()
|
|
||||||
|
|
||||||
view.myanimelist_search_field.addTextChangedListener(object : SimpleTextWatcher() {
|
|
||||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
|
||||||
querySubject.onNext(s.toString())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
// Listen to text changes
|
|
||||||
searchSubscription = querySubject.debounce(1, TimeUnit.SECONDS)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe { search(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
searchSubscription?.unsubscribe()
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onPositiveButtonClick() {
|
|
||||||
presenter.registerManga(selectedItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun search(query: String) {
|
|
||||||
if (!query.isNullOrEmpty()) {
|
|
||||||
v.myanimelist_search_results.visibility = View.GONE
|
|
||||||
v.progress.visibility = View.VISIBLE
|
|
||||||
presenter.searchManga(query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSearchResults(results: List<MangaSync>) {
|
|
||||||
selectedItem = null
|
|
||||||
v.progress.visibility = View.GONE
|
|
||||||
v.myanimelist_search_results.visibility = View.VISIBLE
|
|
||||||
adapter.setItems(results)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSearchResultsError() {
|
|
||||||
v.progress.visibility = View.GONE
|
|
||||||
v.myanimelist_search_results.visibility = View.VISIBLE
|
|
||||||
adapter.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
val malFragment: MyAnimeListFragment
|
|
||||||
get() = parentFragment as MyAnimeListFragment
|
|
||||||
|
|
||||||
val presenter: MyAnimeListPresenter
|
|
||||||
get() = malFragment.presenter
|
|
||||||
|
|
||||||
}
|
|
@ -1,177 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.myanimelist
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.NumberPicker
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
|
||||||
import eu.kanade.tachiyomi.util.toast
|
|
||||||
import kotlinx.android.synthetic.main.card_myanimelist_personal.*
|
|
||||||
import kotlinx.android.synthetic.main.fragment_myanimelist.*
|
|
||||||
import nucleus.factory.RequiresPresenter
|
|
||||||
import java.text.DecimalFormat
|
|
||||||
|
|
||||||
@RequiresPresenter(MyAnimeListPresenter::class)
|
|
||||||
class MyAnimeListFragment : BaseRxFragment<MyAnimeListPresenter>() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun newInstance(): MyAnimeListFragment {
|
|
||||||
return MyAnimeListFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var dialog: MyAnimeListDialogFragment? = null
|
|
||||||
|
|
||||||
private val decimalFormat = DecimalFormat("#.##")
|
|
||||||
|
|
||||||
private val SEARCH_FRAGMENT_TAG = "mal_search"
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
|
|
||||||
return inflater.inflate(R.layout.fragment_myanimelist, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
|
||||||
swipe_refresh.isEnabled = false
|
|
||||||
swipe_refresh.setOnRefreshListener { presenter.refresh() }
|
|
||||||
myanimelist_title_layout.setOnClickListener { onTitleClick() }
|
|
||||||
myanimelist_status_layout.setOnClickListener { onStatusClick() }
|
|
||||||
myanimelist_chapters_layout.setOnClickListener { onChaptersClick() }
|
|
||||||
myanimelist_score_layout.setOnClickListener { onScoreClick() }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
fun setMangaSync(mangaSync: MangaSync?) {
|
|
||||||
swipe_refresh.isEnabled = mangaSync != null
|
|
||||||
mangaSync?.let {
|
|
||||||
myanimelist_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary)
|
|
||||||
myanimelist_title.setAllCaps(false)
|
|
||||||
myanimelist_title.text = it.title
|
|
||||||
myanimelist_chapters.text = if (it.total_chapters > 0)
|
|
||||||
"${it.last_chapter_read}/${it.total_chapters}" else "${it.last_chapter_read}/-"
|
|
||||||
myanimelist_score.text = if (it.score == 0f) "-" else decimalFormat.format(it.score)
|
|
||||||
myanimelist_status.text = presenter.myAnimeList.getStatus(it.status)
|
|
||||||
} ?: run {
|
|
||||||
myanimelist_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button)
|
|
||||||
myanimelist_title.setText(R.string.action_edit)
|
|
||||||
myanimelist_chapters.text = ""
|
|
||||||
myanimelist_score.text = ""
|
|
||||||
myanimelist_status.text = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onRefreshDone() {
|
|
||||||
swipe_refresh.isRefreshing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onRefreshError(error: Throwable) {
|
|
||||||
swipe_refresh.isRefreshing = false
|
|
||||||
context.toast(error.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSearchResults(results: List<MangaSync>) {
|
|
||||||
findSearchFragmentIfNeeded()
|
|
||||||
|
|
||||||
dialog?.onSearchResults(results)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSearchResultsError(error: Throwable) {
|
|
||||||
findSearchFragmentIfNeeded()
|
|
||||||
context.toast(error.message)
|
|
||||||
|
|
||||||
dialog?.onSearchResultsError()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findSearchFragmentIfNeeded() {
|
|
||||||
if (dialog == null) {
|
|
||||||
dialog = childFragmentManager.findFragmentByTag(SEARCH_FRAGMENT_TAG) as MyAnimeListDialogFragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onTitleClick() {
|
|
||||||
if (dialog == null) {
|
|
||||||
dialog = MyAnimeListDialogFragment.newInstance()
|
|
||||||
}
|
|
||||||
|
|
||||||
presenter.restartSearch()
|
|
||||||
dialog?.show(childFragmentManager, SEARCH_FRAGMENT_TAG)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onStatusClick() {
|
|
||||||
if (presenter.mangaSync == null)
|
|
||||||
return
|
|
||||||
|
|
||||||
MaterialDialog.Builder(activity)
|
|
||||||
.title(R.string.status)
|
|
||||||
.items(presenter.getAllStatus())
|
|
||||||
.itemsCallbackSingleChoice(presenter.getIndexFromStatus(), { dialog, view, i, charSequence ->
|
|
||||||
presenter.setStatus(i)
|
|
||||||
myanimelist_status.text = "..."
|
|
||||||
true
|
|
||||||
})
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onChaptersClick() {
|
|
||||||
if (presenter.mangaSync == null)
|
|
||||||
return
|
|
||||||
|
|
||||||
val dialog = MaterialDialog.Builder(activity)
|
|
||||||
.title(R.string.chapters)
|
|
||||||
.customView(R.layout.dialog_myanimelist_chapters, false)
|
|
||||||
.positiveText(android.R.string.ok)
|
|
||||||
.negativeText(android.R.string.cancel)
|
|
||||||
.onPositive { d, action ->
|
|
||||||
val view = d.customView
|
|
||||||
if (view != null) {
|
|
||||||
val np = view.findViewById(R.id.chapters_picker) as NumberPicker
|
|
||||||
np.clearFocus()
|
|
||||||
presenter.setLastChapterRead(np.value)
|
|
||||||
myanimelist_chapters.text = "..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
|
|
||||||
val view = dialog.customView
|
|
||||||
if (view != null) {
|
|
||||||
val np = view.findViewById(R.id.chapters_picker) as NumberPicker
|
|
||||||
// Set initial value
|
|
||||||
np.value = presenter.mangaSync!!.last_chapter_read
|
|
||||||
// Don't allow to go from 0 to 9999
|
|
||||||
np.wrapSelectorWheel = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onScoreClick() {
|
|
||||||
if (presenter.mangaSync == null)
|
|
||||||
return
|
|
||||||
|
|
||||||
val dialog = MaterialDialog.Builder(activity)
|
|
||||||
.title(R.string.score)
|
|
||||||
.customView(R.layout.dialog_myanimelist_score, false)
|
|
||||||
.positiveText(android.R.string.ok)
|
|
||||||
.negativeText(android.R.string.cancel)
|
|
||||||
.onPositive { d, action ->
|
|
||||||
val view = d.customView
|
|
||||||
if (view != null) {
|
|
||||||
val np = view.findViewById(R.id.score_picker) as NumberPicker
|
|
||||||
np.clearFocus()
|
|
||||||
presenter.setScore(np.value)
|
|
||||||
myanimelist_score.text = "..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
|
|
||||||
val view = dialog.customView
|
|
||||||
if (view != null) {
|
|
||||||
val np = view.findViewById(R.id.score_picker) as NumberPicker
|
|
||||||
// Set initial value
|
|
||||||
np.value = presenter.mangaSync!!.score.toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,174 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.myanimelist
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
|
||||||
import eu.kanade.tachiyomi.util.SharedData
|
|
||||||
import eu.kanade.tachiyomi.util.toast
|
|
||||||
import rx.Observable
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import timber.log.Timber
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class MyAnimeListPresenter : BasePresenter<MyAnimeListFragment>() {
|
|
||||||
|
|
||||||
val db: DatabaseHelper by injectLazy()
|
|
||||||
val syncManager: MangaSyncManager by injectLazy()
|
|
||||||
|
|
||||||
val myAnimeList by lazy { syncManager.myAnimeList }
|
|
||||||
|
|
||||||
lateinit var manga: Manga
|
|
||||||
private set
|
|
||||||
|
|
||||||
var mangaSync: MangaSync? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
private var query: String? = null
|
|
||||||
|
|
||||||
private val GET_MANGA_SYNC = 1
|
|
||||||
private val GET_SEARCH_RESULTS = 2
|
|
||||||
private val REFRESH = 3
|
|
||||||
|
|
||||||
private val PREFIX_MY = "my:"
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
|
|
||||||
startableLatestCache(GET_MANGA_SYNC,
|
|
||||||
{ db.getMangaSync(manga, myAnimeList).asRxObservable()
|
|
||||||
.doOnNext { mangaSync = it }
|
|
||||||
.observeOn(AndroidSchedulers.mainThread()) },
|
|
||||||
{ view, mangaSync -> view.setMangaSync(mangaSync) })
|
|
||||||
|
|
||||||
startableLatestCache(GET_SEARCH_RESULTS,
|
|
||||||
{ getSearchResultsObservable() },
|
|
||||||
{ view, results -> view.setSearchResults(results) },
|
|
||||||
{ view, error -> view.setSearchResultsError(error) })
|
|
||||||
|
|
||||||
startableFirst(REFRESH,
|
|
||||||
{ getRefreshObservable() },
|
|
||||||
{ view, result -> view.onRefreshDone() },
|
|
||||||
{ view, error -> view.onRefreshError(error) })
|
|
||||||
|
|
||||||
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
|
|
||||||
start(GET_MANGA_SYNC)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSearchResultsObservable(): Observable<List<MangaSync>> {
|
|
||||||
return query?.let { query ->
|
|
||||||
val observable: Observable<List<MangaSync>>
|
|
||||||
if (query.startsWith(PREFIX_MY)) {
|
|
||||||
val realQuery = query.substring(PREFIX_MY.length).toLowerCase().trim()
|
|
||||||
observable = myAnimeList.getList()
|
|
||||||
.flatMap { Observable.from(it) }
|
|
||||||
.filter { it.title.toLowerCase().contains(realQuery) }
|
|
||||||
.toList()
|
|
||||||
} else {
|
|
||||||
observable = myAnimeList.search(query)
|
|
||||||
}
|
|
||||||
observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
|
|
||||||
} ?: Observable.error(Exception("Null query"))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getRefreshObservable(): Observable<PutResult> {
|
|
||||||
return mangaSync?.let { mangaSync ->
|
|
||||||
myAnimeList.getList()
|
|
||||||
.map { myList ->
|
|
||||||
myList.find { it.remote_id == mangaSync.remote_id }?.let {
|
|
||||||
mangaSync.copyPersonalFrom(it)
|
|
||||||
mangaSync.total_chapters = it.total_chapters
|
|
||||||
mangaSync
|
|
||||||
} ?: throw Exception("Could not find manga")
|
|
||||||
}
|
|
||||||
.flatMap { db.insertMangaSync(it).asRxObservable() }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
} ?: Observable.error(Exception("Not found"))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateRemote() {
|
|
||||||
mangaSync?.let { mangaSync ->
|
|
||||||
add(myAnimeList.update(mangaSync)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.flatMap { db.insertMangaSync(mangaSync).asRxObservable() }
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe({ next -> },
|
|
||||||
{ error ->
|
|
||||||
Timber.e(error)
|
|
||||||
// Restart on error to set old values
|
|
||||||
start(GET_MANGA_SYNC)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun searchManga(query: String) {
|
|
||||||
if (query.isNullOrEmpty() || query == this.query)
|
|
||||||
return
|
|
||||||
|
|
||||||
this.query = query
|
|
||||||
start(GET_SEARCH_RESULTS)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun restartSearch() {
|
|
||||||
query = null
|
|
||||||
stop(GET_SEARCH_RESULTS)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerManga(sync: MangaSync?) {
|
|
||||||
if (sync != null) {
|
|
||||||
sync.manga_id = manga.id!!
|
|
||||||
add(myAnimeList.bind(sync)
|
|
||||||
.flatMap { db.insertMangaSync(sync).asRxObservable() }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe({ },
|
|
||||||
{ error -> context.toast(error.message) }))
|
|
||||||
} else {
|
|
||||||
db.deleteMangaSyncForManga(manga).executeAsBlocking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAllStatus(): List<String> {
|
|
||||||
return listOf(context.getString(R.string.reading),
|
|
||||||
context.getString(R.string.completed),
|
|
||||||
context.getString(R.string.on_hold),
|
|
||||||
context.getString(R.string.dropped),
|
|
||||||
context.getString(R.string.plan_to_read))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getIndexFromStatus(): Int {
|
|
||||||
return mangaSync?.let { mangaSync ->
|
|
||||||
if (mangaSync.status == 6) 4 else mangaSync.status - 1
|
|
||||||
} ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setStatus(index: Int) {
|
|
||||||
mangaSync?.status = if (index == 4) 6 else index + 1
|
|
||||||
updateRemote()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setScore(score: Int) {
|
|
||||||
mangaSync?.score = score.toFloat()
|
|
||||||
updateRemote()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setLastChapterRead(chapterNumber: Int) {
|
|
||||||
mangaSync?.last_chapter_read = chapterNumber
|
|
||||||
updateRemote()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refresh() {
|
|
||||||
if (mangaSync != null) {
|
|
||||||
start(REFRESH)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.myanimelist
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.util.inflate
|
|
||||||
import kotlinx.android.synthetic.main.dialog_myanimelist_search_item.view.*
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class MyAnimeListSearchAdapter(context: Context) :
|
|
||||||
ArrayAdapter<MangaSync>(context, R.layout.dialog_myanimelist_search_item, ArrayList<MangaSync>()) {
|
|
||||||
|
|
||||||
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
|
|
||||||
var v = view
|
|
||||||
// Get the data item for this position
|
|
||||||
val sync = getItem(position)
|
|
||||||
// Check if an existing view is being reused, otherwise inflate the view
|
|
||||||
val holder: SearchViewHolder // view lookup cache stored in tag
|
|
||||||
if (v == null) {
|
|
||||||
v = parent.inflate(R.layout.dialog_myanimelist_search_item)
|
|
||||||
holder = SearchViewHolder(v)
|
|
||||||
v.tag = holder
|
|
||||||
} else {
|
|
||||||
holder = v.tag as SearchViewHolder
|
|
||||||
}
|
|
||||||
holder.onSetValues(sync)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setItems(syncs: List<MangaSync>) {
|
|
||||||
setNotifyOnChange(false)
|
|
||||||
clear()
|
|
||||||
addAll(syncs)
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
class SearchViewHolder(private val view: View) {
|
|
||||||
|
|
||||||
fun onSetValues(sync: MangaSync) {
|
|
||||||
view.myanimelist_result_title.text = sync.title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,33 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.track
|
||||||
|
|
||||||
|
import android.support.v7.widget.RecyclerView
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
|
|
||||||
|
class TrackAdapter(val fragment: TrackFragment) : RecyclerView.Adapter<TrackHolder>() {
|
||||||
|
|
||||||
|
var items = emptyList<TrackItem>()
|
||||||
|
set(value) {
|
||||||
|
if (field !== value) {
|
||||||
|
field = value
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var onClickListener: (TrackItem) -> Unit = {}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return items.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder {
|
||||||
|
val view = parent.inflate(R.layout.item_track)
|
||||||
|
return TrackHolder(view, fragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: TrackHolder, position: Int) {
|
||||||
|
holder.onSetValues(items[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,166 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.track
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v7.widget.LinearLayoutManager
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.NumberPicker
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaActivity
|
||||||
|
import eu.kanade.tachiyomi.util.toast
|
||||||
|
import kotlinx.android.synthetic.main.fragment_track.*
|
||||||
|
import nucleus.factory.RequiresPresenter
|
||||||
|
|
||||||
|
@RequiresPresenter(TrackPresenter::class)
|
||||||
|
class TrackFragment : BaseRxFragment<TrackPresenter>() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance(): TrackFragment {
|
||||||
|
return TrackFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var adapter: TrackAdapter
|
||||||
|
|
||||||
|
private var dialog: TrackSearchDialog? = null
|
||||||
|
|
||||||
|
private val searchFragmentTag: String
|
||||||
|
get() = "search_fragment"
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View {
|
||||||
|
return inflater.inflate(R.layout.fragment_track, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
adapter = TrackAdapter(this)
|
||||||
|
recycler.layoutManager = LinearLayoutManager(context)
|
||||||
|
recycler.adapter = adapter
|
||||||
|
swipe_refresh.isEnabled = false
|
||||||
|
swipe_refresh.setOnRefreshListener { presenter.refresh() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findSearchFragmentIfNeeded() {
|
||||||
|
if (dialog == null) {
|
||||||
|
dialog = childFragmentManager.findFragmentByTag(searchFragmentTag) as TrackSearchDialog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onNextTrackings(trackings: List<TrackItem>) {
|
||||||
|
adapter.items = trackings
|
||||||
|
swipe_refresh.isEnabled = trackings.any { it.track != null }
|
||||||
|
(activity as MangaActivity).setTrackingIcon(trackings.any { it.track != null })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSearchResults(results: List<Track>) {
|
||||||
|
if (!isResumed) return
|
||||||
|
|
||||||
|
findSearchFragmentIfNeeded()
|
||||||
|
dialog?.onSearchResults(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSearchResultsError(error: Throwable) {
|
||||||
|
if (!isResumed) return
|
||||||
|
|
||||||
|
findSearchFragmentIfNeeded()
|
||||||
|
dialog?.onSearchResultsError()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRefreshDone() {
|
||||||
|
swipe_refresh.isRefreshing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRefreshError(error: Throwable) {
|
||||||
|
swipe_refresh.isRefreshing = false
|
||||||
|
context.toast(error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTitleClick(item: TrackItem) {
|
||||||
|
if (!isResumed) return
|
||||||
|
|
||||||
|
if (dialog == null) {
|
||||||
|
dialog = TrackSearchDialog.newInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
presenter.selectedService = item.service
|
||||||
|
dialog?.show(childFragmentManager, searchFragmentTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onStatusClick(item: TrackItem) {
|
||||||
|
if (!isResumed || item.track == null) return
|
||||||
|
|
||||||
|
val statusList = item.service.getStatusList().map { item.service.getStatus(it) }
|
||||||
|
val selectedIndex = item.service.getStatusList().indexOf(item.track.status)
|
||||||
|
|
||||||
|
MaterialDialog.Builder(context)
|
||||||
|
.title(R.string.status)
|
||||||
|
.items(statusList)
|
||||||
|
.itemsCallbackSingleChoice(selectedIndex, { dialog, view, i, charSequence ->
|
||||||
|
presenter.setStatus(item, i)
|
||||||
|
swipe_refresh.isRefreshing = true
|
||||||
|
true
|
||||||
|
})
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onChaptersClick(item: TrackItem) {
|
||||||
|
if (!isResumed || item.track == null) return
|
||||||
|
|
||||||
|
val dialog = MaterialDialog.Builder(context)
|
||||||
|
.title(R.string.chapters)
|
||||||
|
.customView(R.layout.dialog_track_chapters, false)
|
||||||
|
.positiveText(android.R.string.ok)
|
||||||
|
.negativeText(android.R.string.cancel)
|
||||||
|
.onPositive { d, action ->
|
||||||
|
val view = d.customView
|
||||||
|
if (view != null) {
|
||||||
|
val np = view.findViewById(R.id.chapters_picker) as NumberPicker
|
||||||
|
np.clearFocus()
|
||||||
|
presenter.setLastChapterRead(item, np.value)
|
||||||
|
swipe_refresh.isRefreshing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
|
||||||
|
val view = dialog.customView
|
||||||
|
if (view != null) {
|
||||||
|
val np = view.findViewById(R.id.chapters_picker) as NumberPicker
|
||||||
|
// Set initial value
|
||||||
|
np.value = item.track.last_chapter_read
|
||||||
|
// Don't allow to go from 0 to 9999
|
||||||
|
np.wrapSelectorWheel = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onScoreClick(item: TrackItem) {
|
||||||
|
if (!isResumed || item.track == null) return
|
||||||
|
|
||||||
|
val dialog = MaterialDialog.Builder(activity)
|
||||||
|
.title(R.string.score)
|
||||||
|
.customView(R.layout.dialog_track_score, false)
|
||||||
|
.positiveText(android.R.string.ok)
|
||||||
|
.negativeText(android.R.string.cancel)
|
||||||
|
.onPositive { d, action ->
|
||||||
|
val view = d.customView
|
||||||
|
if (view != null) {
|
||||||
|
val np = view.findViewById(R.id.score_picker) as NumberPicker
|
||||||
|
np.clearFocus()
|
||||||
|
presenter.setScore(item, np.value)
|
||||||
|
swipe_refresh.isRefreshing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
|
||||||
|
val view = dialog.customView
|
||||||
|
if (view != null) {
|
||||||
|
val np = view.findViewById(R.id.score_picker) as NumberPicker
|
||||||
|
np.maxValue = item.service.maxScore()
|
||||||
|
// Set initial value
|
||||||
|
np.value = item.track.score.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.track
|
||||||
|
|
||||||
|
import android.support.v7.widget.RecyclerView
|
||||||
|
import android.view.View
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import kotlinx.android.synthetic.main.item_track.view.*
|
||||||
|
|
||||||
|
class TrackHolder(private val view: View, private val fragment: TrackFragment)
|
||||||
|
: RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
|
private lateinit var item: TrackItem
|
||||||
|
|
||||||
|
init {
|
||||||
|
view.title_container.setOnClickListener { fragment.onTitleClick(item) }
|
||||||
|
view.status_container.setOnClickListener { fragment.onStatusClick(item) }
|
||||||
|
view.chapters_container.setOnClickListener { fragment.onChaptersClick(item) }
|
||||||
|
view.score_container.setOnClickListener { fragment.onScoreClick(item) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
fun onSetValues(item: TrackItem) = with(view) {
|
||||||
|
this@TrackHolder.item = item
|
||||||
|
val track = item.track
|
||||||
|
track_logo.setImageResource(item.service.getLogo())
|
||||||
|
logo.setBackgroundColor(item.service.getLogoColor())
|
||||||
|
if (track != null) {
|
||||||
|
track_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary)
|
||||||
|
track_title.setAllCaps(false)
|
||||||
|
track_title.text = track.title
|
||||||
|
track_chapters.text = "${track.last_chapter_read}/" +
|
||||||
|
if (track.total_chapters > 0) track.total_chapters else "-"
|
||||||
|
track_status.text = item.service.getStatus(track.status)
|
||||||
|
track_score.text = if (track.score == 0f) "-" else item.service.formatScore(track)
|
||||||
|
} else {
|
||||||
|
track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button)
|
||||||
|
track_title.setText(R.string.action_edit)
|
||||||
|
track_chapters.text = ""
|
||||||
|
track_score.text = ""
|
||||||
|
track_status.text = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.track
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
|
||||||
|
class TrackItem(val track: Track?, val service: TrackService) {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,137 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.track
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
||||||
|
import eu.kanade.tachiyomi.util.SharedData
|
||||||
|
import eu.kanade.tachiyomi.util.toast
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Subscription
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
import rx.schedulers.Schedulers
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class TrackPresenter : BasePresenter<TrackFragment>() {
|
||||||
|
|
||||||
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
|
private val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
|
lateinit var manga: Manga
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var trackList: List<TrackItem> = emptyList()
|
||||||
|
|
||||||
|
private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||||
|
|
||||||
|
var selectedService: TrackService? = null
|
||||||
|
|
||||||
|
private var trackSubscription: Subscription? = null
|
||||||
|
|
||||||
|
private var searchSubscription: Subscription? = null
|
||||||
|
|
||||||
|
private var refreshSubscription: Subscription? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedState: Bundle?) {
|
||||||
|
super.onCreate(savedState)
|
||||||
|
|
||||||
|
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
|
||||||
|
fetchTrackings()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchTrackings() {
|
||||||
|
trackSubscription?.let { remove(it) }
|
||||||
|
trackSubscription = db.getTracks(manga)
|
||||||
|
.asRxObservable()
|
||||||
|
.map { tracks ->
|
||||||
|
loggedServices.map { service ->
|
||||||
|
TrackItem(tracks.find { it.sync_id == service.id }, service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doOnNext { trackList = it }
|
||||||
|
.subscribeLatestCache(TrackFragment::onNextTrackings)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
refreshSubscription?.let { remove(it) }
|
||||||
|
refreshSubscription = Observable.from(trackList)
|
||||||
|
.filter { it.track != null }
|
||||||
|
.concatMap { item ->
|
||||||
|
item.service.refresh(item.track!!)
|
||||||
|
.flatMap { db.insertTrack(it).asRxObservable() }
|
||||||
|
.map { item }
|
||||||
|
.onErrorReturn { item }
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeFirst({ view, result -> view.onRefreshDone() },
|
||||||
|
TrackFragment::onRefreshError)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(query: String) {
|
||||||
|
val service = selectedService ?: return
|
||||||
|
|
||||||
|
searchSubscription?.let { remove(it) }
|
||||||
|
searchSubscription = service.search(query)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeLatestCache(TrackFragment::onSearchResults,
|
||||||
|
TrackFragment::onSearchResultsError)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerTracking(item: Track?) {
|
||||||
|
val service = selectedService ?: return
|
||||||
|
|
||||||
|
if (item != null) {
|
||||||
|
item.manga_id = manga.id!!
|
||||||
|
add(service.bind(item)
|
||||||
|
.flatMap { db.insertTrack(item).asRxObservable() }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({ },
|
||||||
|
{ error -> context.toast(error.message) }))
|
||||||
|
} else {
|
||||||
|
db.deleteTrackForManga(manga, service).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateRemote(track: Track, service: TrackService) {
|
||||||
|
service.update(track)
|
||||||
|
.flatMap { db.insertTrack(track).asRxObservable() }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeFirst({ view, result -> view.onRefreshDone() },
|
||||||
|
{ view, error ->
|
||||||
|
view.onRefreshError(error)
|
||||||
|
|
||||||
|
// Restart on error to set old values
|
||||||
|
fetchTrackings()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setStatus(item: TrackItem, index: Int) {
|
||||||
|
val track = item.track!!
|
||||||
|
track.status = item.service.getStatusList()[index]
|
||||||
|
updateRemote(track, item.service)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setScore(item: TrackItem, score: Int) {
|
||||||
|
val track = item.track!!
|
||||||
|
track.score = score.toFloat()
|
||||||
|
updateRemote(track, item.service)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLastChapterRead(item: TrackItem, chapterNumber: Int) {
|
||||||
|
val track = item.track!!
|
||||||
|
track.last_chapter_read = chapterNumber
|
||||||
|
updateRemote(track, item.service)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.track
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
|
import kotlinx.android.synthetic.main.item_track_search.view.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class TrackSearchAdapter(context: Context)
|
||||||
|
: ArrayAdapter<Track>(context, R.layout.item_track_search, ArrayList<Track>()) {
|
||||||
|
|
||||||
|
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
|
||||||
|
var v = view
|
||||||
|
// Get the data item for this position
|
||||||
|
val track = getItem(position)
|
||||||
|
// Check if an existing view is being reused, otherwise inflate the view
|
||||||
|
val holder: TrackSearchHolder // view lookup cache stored in tag
|
||||||
|
if (v == null) {
|
||||||
|
v = parent.inflate(R.layout.item_track_search)
|
||||||
|
holder = TrackSearchHolder(v)
|
||||||
|
v.tag = holder
|
||||||
|
} else {
|
||||||
|
holder = v.tag as TrackSearchHolder
|
||||||
|
}
|
||||||
|
holder.onSetValues(track)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setItems(syncs: List<Track>) {
|
||||||
|
setNotifyOnChange(false)
|
||||||
|
clear()
|
||||||
|
addAll(syncs)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrackSearchHolder(private val view: View) {
|
||||||
|
|
||||||
|
fun onSetValues(track: Track) {
|
||||||
|
view.track_search_title.text = track.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.track
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v4.app.DialogFragment
|
||||||
|
import android.view.View
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
|
||||||
|
import kotlinx.android.synthetic.main.dialog_track_search.view.*
|
||||||
|
import rx.Subscription
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class TrackSearchDialog : DialogFragment() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun newInstance(): TrackSearchDialog {
|
||||||
|
return TrackSearchDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var v: View
|
||||||
|
|
||||||
|
lateinit var adapter: TrackSearchAdapter
|
||||||
|
private set
|
||||||
|
|
||||||
|
private val queryRelay by lazy { PublishRelay.create<String>() }
|
||||||
|
|
||||||
|
private var searchDebounceSubscription: Subscription? = null
|
||||||
|
|
||||||
|
private var selectedItem: Track? = null
|
||||||
|
|
||||||
|
val presenter: TrackPresenter
|
||||||
|
get() = (parentFragment as TrackFragment).presenter
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedState: Bundle?): Dialog {
|
||||||
|
val dialog = MaterialDialog.Builder(context)
|
||||||
|
.customView(R.layout.dialog_track_search, false)
|
||||||
|
.positiveText(android.R.string.ok)
|
||||||
|
.negativeText(android.R.string.cancel)
|
||||||
|
.onPositive { dialog1, which -> onPositiveButtonClick() }
|
||||||
|
.build()
|
||||||
|
|
||||||
|
onViewCreated(dialog.view, savedState)
|
||||||
|
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||||
|
v = view
|
||||||
|
|
||||||
|
// Create adapter
|
||||||
|
adapter = TrackSearchAdapter(context)
|
||||||
|
view.track_search_list.adapter = adapter
|
||||||
|
|
||||||
|
// Set listeners
|
||||||
|
selectedItem = null
|
||||||
|
view.track_search_list.setOnItemClickListener { parent, viewList, position, id ->
|
||||||
|
selectedItem = adapter.getItem(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do an initial search based on the manga's title
|
||||||
|
if (savedState == null) {
|
||||||
|
val title = presenter.manga.title
|
||||||
|
view.track_search.append(title)
|
||||||
|
search(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.track_search.addTextChangedListener(object : SimpleTextWatcher() {
|
||||||
|
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||||
|
queryRelay.call(s.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// Listen to text changes
|
||||||
|
searchDebounceSubscription = queryRelay.debounce(1, TimeUnit.SECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.subscribe { search(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
searchDebounceSubscription?.unsubscribe()
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun search(query: String) {
|
||||||
|
v.progress.visibility = View.VISIBLE
|
||||||
|
v.track_search_list.visibility = View.GONE
|
||||||
|
|
||||||
|
presenter.search(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSearchResults(results: List<Track>) {
|
||||||
|
selectedItem = null
|
||||||
|
v.progress.visibility = View.GONE
|
||||||
|
v.track_search_list.visibility = View.VISIBLE
|
||||||
|
adapter.setItems(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSearchResultsError() {
|
||||||
|
v.progress.visibility = View.VISIBLE
|
||||||
|
v.track_search_list.visibility = View.GONE
|
||||||
|
adapter.setItems(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onPositiveButtonClick() {
|
||||||
|
presenter.registerTracking(selectedItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -163,19 +163,19 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
val chapterToUpdate = presenter.getMangaSyncChapterToUpdate()
|
val chapterToUpdate = presenter.getTrackChapterToUpdate()
|
||||||
|
|
||||||
if (chapterToUpdate > 0) {
|
if (chapterToUpdate > 0) {
|
||||||
if (preferences.askUpdateMangaSync()) {
|
if (preferences.askUpdateTrack()) {
|
||||||
MaterialDialog.Builder(this)
|
MaterialDialog.Builder(this)
|
||||||
.content(getString(R.string.confirm_update_manga_sync, chapterToUpdate))
|
.content(getString(R.string.confirm_update_manga_sync, chapterToUpdate))
|
||||||
.positiveText(android.R.string.yes)
|
.positiveText(android.R.string.yes)
|
||||||
.negativeText(android.R.string.no)
|
.negativeText(android.R.string.no)
|
||||||
.onPositive { dialog, which -> presenter.updateMangaSyncLastChapterRead() }
|
.onPositive { dialog, which -> presenter.updateTrackLastChapterRead() }
|
||||||
.onAny { dialog1, which1 -> super.onBackPressed() }
|
.onAny { dialog1, which1 -> super.onBackPressed() }
|
||||||
.show()
|
.show()
|
||||||
} else {
|
} else {
|
||||||
presenter.updateMangaSyncLastChapterRead()
|
presenter.updateTrackLastChapterRead()
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -10,14 +10,14 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackUpdateService
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.ui.reader.notification.ImageNotifier
|
import eu.kanade.tachiyomi.ui.reader.notification.ImageNotifier
|
||||||
import eu.kanade.tachiyomi.util.DiskUtil
|
import eu.kanade.tachiyomi.util.DiskUtil
|
||||||
@ -54,9 +54,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
val downloadManager: DownloadManager by injectLazy()
|
val downloadManager: DownloadManager by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync manager.
|
* Tracking manager.
|
||||||
*/
|
*/
|
||||||
val syncManager: MangaSyncManager by injectLazy()
|
val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Source manager.
|
* Source manager.
|
||||||
@ -124,7 +124,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
/**
|
/**
|
||||||
* List of manga services linked to the active manga, or null if auto syncing is not enabled.
|
* List of manga services linked to the active manga, or null if auto syncing is not enabled.
|
||||||
*/
|
*/
|
||||||
private var mangaSyncList: List<MangaSync>? = null
|
private var trackList: List<Track>? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chapter loader whose job is to obtain the chapter list and initialize every page.
|
* Chapter loader whose job is to obtain the chapter list and initialize every page.
|
||||||
@ -165,9 +165,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
.subscribeLatestCache({ view, manga -> view.onMangaOpen(manga) })
|
.subscribeLatestCache({ view, manga -> view.onMangaOpen(manga) })
|
||||||
|
|
||||||
// Retrieve the sync list if auto syncing is enabled.
|
// Retrieve the sync list if auto syncing is enabled.
|
||||||
if (prefs.autoUpdateMangaSync()) {
|
if (prefs.autoUpdateTrack()) {
|
||||||
add(db.getMangasSync(manga).asRxSingle()
|
add(db.getTracks(manga).asRxSingle()
|
||||||
.subscribe({ mangaSyncList = it }))
|
.subscribe({ trackList = it }))
|
||||||
}
|
}
|
||||||
|
|
||||||
restartableLatestCache(LOAD_ACTIVE_CHAPTER,
|
restartableLatestCache(LOAD_ACTIVE_CHAPTER,
|
||||||
@ -431,9 +431,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
/**
|
/**
|
||||||
* Returns the chapter to be marked as last read in sync services or 0 if no update required.
|
* Returns the chapter to be marked as last read in sync services or 0 if no update required.
|
||||||
*/
|
*/
|
||||||
fun getMangaSyncChapterToUpdate(): Int {
|
fun getTrackChapterToUpdate(): Int {
|
||||||
val mangaSyncList = mangaSyncList
|
val trackList = trackList
|
||||||
if (chapter.pages == null || mangaSyncList == null || mangaSyncList.isEmpty())
|
if (chapter.pages == null || trackList == null || trackList.isEmpty())
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
val prevChapter = prevChapter
|
val prevChapter = prevChapter
|
||||||
@ -446,24 +446,24 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
else
|
else
|
||||||
0
|
0
|
||||||
|
|
||||||
mangaSyncList.forEach { sync ->
|
trackList.forEach { sync ->
|
||||||
if (lastChapterRead > sync.last_chapter_read) {
|
if (lastChapterRead > sync.last_chapter_read) {
|
||||||
sync.last_chapter_read = lastChapterRead
|
sync.last_chapter_read = lastChapterRead
|
||||||
sync.update = true
|
sync.update = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (mangaSyncList.any { it.update }) lastChapterRead else 0
|
return if (trackList.any { it.update }) lastChapterRead else 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the service that updates the last chapter read in sync services
|
* Starts the service that updates the last chapter read in sync services
|
||||||
*/
|
*/
|
||||||
fun updateMangaSyncLastChapterRead() {
|
fun updateTrackLastChapterRead() {
|
||||||
mangaSyncList?.forEach { sync ->
|
trackList?.forEach { sync ->
|
||||||
val service = syncManager.getService(sync.sync_id)
|
val service = trackManager.getService(sync.sync_id)
|
||||||
if (service != null && service.isLogged && sync.update) {
|
if (service != null && service.isLogged && sync.update) {
|
||||||
UpdateMangaSyncService.start(context, sync)
|
TrackUpdateService.start(context, sync)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,14 @@ import android.view.Gravity.CENTER
|
|||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class AnilistLoginActivity : AppCompatActivity() {
|
class AnilistLoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private val syncManager: MangaSyncManager by injectLazy()
|
private val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
@ -24,7 +24,7 @@ class AnilistLoginActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val code = intent.data?.getQueryParameter("code")
|
val code = intent.data?.getQueryParameter("code")
|
||||||
if (code != null) {
|
if (code != null) {
|
||||||
syncManager.aniList.login(code)
|
trackManager.aniList.login(code)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
@ -33,7 +33,7 @@ class AnilistLoginActivity : AppCompatActivity() {
|
|||||||
returnToSettings()
|
returnToSettings()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
syncManager.aniList.logout()
|
trackManager.aniList.logout()
|
||||||
returnToSettings()
|
returnToSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ class SettingsActivity : BaseActivity(),
|
|||||||
"general_screen" -> SettingsGeneralFragment.newInstance(key)
|
"general_screen" -> SettingsGeneralFragment.newInstance(key)
|
||||||
"downloads_screen" -> SettingsDownloadsFragment.newInstance(key)
|
"downloads_screen" -> SettingsDownloadsFragment.newInstance(key)
|
||||||
"sources_screen" -> SettingsSourcesFragment.newInstance(key)
|
"sources_screen" -> SettingsSourcesFragment.newInstance(key)
|
||||||
"sync_screen" -> SettingsSyncFragment.newInstance(key)
|
"tracking_screen" -> SettingsTrackingFragment.newInstance(key)
|
||||||
"advanced_screen" -> SettingsAdvancedFragment.newInstance(key)
|
"advanced_screen" -> SettingsAdvancedFragment.newInstance(key)
|
||||||
"about_screen" -> SettingsAboutFragment.newInstance(key)
|
"about_screen" -> SettingsAboutFragment.newInstance(key)
|
||||||
else -> SettingsFragment.newInstance(key)
|
else -> SettingsFragment.newInstance(key)
|
||||||
|
@ -28,7 +28,7 @@ open class SettingsFragment : XpPreferenceFragment() {
|
|||||||
addPreferencesFromResource(R.xml.pref_reader)
|
addPreferencesFromResource(R.xml.pref_reader)
|
||||||
addPreferencesFromResource(R.xml.pref_downloads)
|
addPreferencesFromResource(R.xml.pref_downloads)
|
||||||
addPreferencesFromResource(R.xml.pref_sources)
|
addPreferencesFromResource(R.xml.pref_sources)
|
||||||
addPreferencesFromResource(R.xml.pref_sync)
|
addPreferencesFromResource(R.xml.pref_tracking)
|
||||||
addPreferencesFromResource(R.xml.pref_advanced)
|
addPreferencesFromResource(R.xml.pref_advanced)
|
||||||
addPreferencesFromResource(R.xml.pref_about)
|
addPreferencesFromResource(R.xml.pref_about)
|
||||||
|
|
||||||
|
@ -1,89 +1,94 @@
|
|||||||
package eu.kanade.tachiyomi.ui.setting
|
package eu.kanade.tachiyomi.ui.setting
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v7.preference.PreferenceCategory
|
import android.support.customtabs.CustomTabsIntent
|
||||||
import android.support.v7.preference.XpPreferenceFragment
|
import android.support.v7.preference.PreferenceCategory
|
||||||
import android.view.View
|
import android.support.v7.preference.XpPreferenceFragment
|
||||||
import eu.kanade.tachiyomi.R
|
import android.view.View
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.widget.preference.LoginPreference
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.widget.preference.MangaSyncLoginDialog
|
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
||||||
import uy.kohesive.injekt.injectLazy
|
import eu.kanade.tachiyomi.util.getResourceColor
|
||||||
|
import eu.kanade.tachiyomi.widget.preference.LoginPreference
|
||||||
class SettingsSyncFragment : SettingsFragment() {
|
import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
companion object {
|
|
||||||
const val SYNC_CHANGE_REQUEST = 121
|
class SettingsTrackingFragment : SettingsFragment() {
|
||||||
|
|
||||||
fun newInstance(rootKey: String): SettingsSyncFragment {
|
companion object {
|
||||||
val args = Bundle()
|
const val SYNC_CHANGE_REQUEST = 121
|
||||||
args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey)
|
|
||||||
return SettingsSyncFragment().apply { arguments = args }
|
fun newInstance(rootKey: String): SettingsTrackingFragment {
|
||||||
}
|
val args = Bundle()
|
||||||
}
|
args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey)
|
||||||
|
return SettingsTrackingFragment().apply { arguments = args }
|
||||||
private val syncManager: MangaSyncManager by injectLazy()
|
}
|
||||||
|
}
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
|
||||||
|
private val trackManager: TrackManager by injectLazy()
|
||||||
val syncCategory: PreferenceCategory by bindPref(R.string.pref_category_manga_sync_accounts_key)
|
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedState)
|
val syncCategory: PreferenceCategory by bindPref(R.string.pref_category_tracking_accounts_key)
|
||||||
|
|
||||||
registerService(syncManager.myAnimeList)
|
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedState)
|
||||||
// registerService(syncManager.aniList) {
|
|
||||||
// val intent = CustomTabsIntent.Builder()
|
registerService(trackManager.myAnimeList)
|
||||||
// .setToolbarColor(activity.theme.getResourceColor(R.attr.colorPrimary))
|
|
||||||
// .build()
|
registerService(trackManager.aniList) {
|
||||||
// intent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
val intent = CustomTabsIntent.Builder()
|
||||||
// intent.launchUrl(activity, AnilistApi.authUrl())
|
.setToolbarColor(activity.theme.getResourceColor(R.attr.colorPrimary))
|
||||||
// }
|
.build()
|
||||||
}
|
intent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||||
|
intent.launchUrl(activity, AnilistApi.authUrl())
|
||||||
private fun <T : MangaSyncService> registerService(
|
}
|
||||||
service: T,
|
|
||||||
onPreferenceClick: (T) -> Unit = defaultOnPreferenceClick) {
|
registerService(trackManager.kitsu)
|
||||||
|
}
|
||||||
LoginPreference(preferenceManager.context).apply {
|
|
||||||
key = preferences.keys.syncUsername(service.id)
|
private fun <T : TrackService> registerService(
|
||||||
title = service.name
|
service: T,
|
||||||
|
onPreferenceClick: (T) -> Unit = defaultOnPreferenceClick) {
|
||||||
setOnPreferenceClickListener {
|
|
||||||
onPreferenceClick(service)
|
LoginPreference(preferenceManager.context).apply {
|
||||||
true
|
key = preferences.keys.trackUsername(service.id)
|
||||||
}
|
title = service.name
|
||||||
|
|
||||||
syncCategory.addPreference(this)
|
setOnPreferenceClickListener {
|
||||||
}
|
onPreferenceClick(service)
|
||||||
}
|
true
|
||||||
|
}
|
||||||
private val defaultOnPreferenceClick: (MangaSyncService) -> Unit
|
|
||||||
get() = {
|
syncCategory.addPreference(this)
|
||||||
val fragment = MangaSyncLoginDialog.newInstance(it)
|
}
|
||||||
fragment.setTargetFragment(this, SYNC_CHANGE_REQUEST)
|
}
|
||||||
fragment.show(fragmentManager, null)
|
|
||||||
}
|
private val defaultOnPreferenceClick: (TrackService) -> Unit
|
||||||
|
get() = {
|
||||||
override fun onResume() {
|
val fragment = TrackLoginDialog.newInstance(it)
|
||||||
super.onResume()
|
fragment.setTargetFragment(this, SYNC_CHANGE_REQUEST)
|
||||||
// Manually refresh anilist holder
|
fragment.show(fragmentManager, null)
|
||||||
// updatePreference(syncManager.aniList.id)
|
}
|
||||||
}
|
|
||||||
|
override fun onResume() {
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
super.onResume()
|
||||||
if (requestCode == SYNC_CHANGE_REQUEST) {
|
// Manually refresh anilist holder
|
||||||
updatePreference(resultCode)
|
updatePreference(trackManager.aniList.id)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
private fun updatePreference(id: Int) {
|
if (requestCode == SYNC_CHANGE_REQUEST) {
|
||||||
val pref = findPreference(preferences.keys.syncUsername(id)) as? LoginPreference
|
updatePreference(resultCode)
|
||||||
pref?.notifyChanged()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
private fun updatePreference(id: Int) {
|
||||||
|
val pref = findPreference(preferences.keys.trackUsername(id)) as? LoginPreference
|
||||||
|
pref?.notifyChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -3,20 +3,20 @@ package eu.kanade.tachiyomi.widget.preference
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.toast
|
||||||
import kotlinx.android.synthetic.main.pref_account_login.view.*
|
import kotlinx.android.synthetic.main.pref_account_login.view.*
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class MangaSyncLoginDialog : LoginDialogPreference() {
|
class TrackLoginDialog : LoginDialogPreference() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun newInstance(sync: MangaSyncService): LoginDialogPreference {
|
fun newInstance(sync: TrackService): LoginDialogPreference {
|
||||||
val fragment = MangaSyncLoginDialog()
|
val fragment = TrackLoginDialog()
|
||||||
val bundle = Bundle(1)
|
val bundle = Bundle(1)
|
||||||
bundle.putInt("key", sync.id)
|
bundle.putInt("key", sync.id)
|
||||||
fragment.arguments = bundle
|
fragment.arguments = bundle
|
||||||
@ -24,15 +24,15 @@ class MangaSyncLoginDialog : LoginDialogPreference() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val syncManager: MangaSyncManager by injectLazy()
|
val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
lateinit var sync: MangaSyncService
|
lateinit var sync: TrackService
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val syncId = arguments.getInt("key")
|
val syncId = arguments.getInt("key")
|
||||||
sync = syncManager.getService(syncId)!!
|
sync = trackManager.getService(syncId)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setCredentialsOnView(view: View) = with(view) {
|
override fun setCredentialsOnView(view: View) = with(view) {
|
||||||
@ -56,11 +56,9 @@ class MangaSyncLoginDialog : LoginDialogPreference() {
|
|||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
sync.saveCredentials(user, pass)
|
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
context.toast(R.string.login_success)
|
context.toast(R.string.login_success)
|
||||||
}, { error ->
|
}, { error ->
|
||||||
sync.logout()
|
|
||||||
login.progress = -1
|
login.progress = -1
|
||||||
login.setText(R.string.unknown_error)
|
login.setText(R.string.unknown_error)
|
||||||
})
|
})
|
BIN
app/src/main/res/drawable-xxxhdpi/al.png
Normal file
BIN
app/src/main/res/drawable-xxxhdpi/al.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/kitsu.png
Normal file
BIN
app/src/main/res/drawable-xxxhdpi/kitsu.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/mal.png
Normal file
BIN
app/src/main/res/drawable-xxxhdpi/mal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
9
app/src/main/res/drawable/ic_done_white_18dp.xml
Normal file
9
app/src/main/res/drawable/ic_done_white_18dp.xml
Normal 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>
|
@ -1,149 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<android.support.v7.widget.CardView
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/cv_mal"
|
|
||||||
style="@style/Theme.Widget.CardView"
|
|
||||||
>
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="@dimen/card_margin">
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:id="@+id/myanimelist_title_layout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
|
||||||
android:background="?attr/selectable_list_drawable"
|
|
||||||
android:clickable="true"
|
|
||||||
android:paddingLeft="?android:listPreferredItemPaddingLeft"
|
|
||||||
android:paddingRight="?android:listPreferredItemPaddingRight">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
style="@style/TextAppearance.Regular.Body1"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:text="Title"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/myanimelist_title"
|
|
||||||
style="@style/TextAppearance.Medium.Button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:text="@string/action_edit"/>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/divider1"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:layout_below="@id/myanimelist_title_layout"
|
|
||||||
android:background="?android:attr/divider"/>
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:id="@+id/myanimelist_status_layout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
|
||||||
android:layout_below="@id/divider1"
|
|
||||||
android:background="?attr/selectable_list_drawable"
|
|
||||||
android:clickable="true"
|
|
||||||
android:paddingLeft="?android:listPreferredItemPaddingLeft"
|
|
||||||
android:paddingRight="?android:listPreferredItemPaddingRight"
|
|
||||||
>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
style="@style/TextAppearance.Regular.Body1"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:text="Status"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/myanimelist_status"
|
|
||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
tools:text="Reading"/>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/divider2"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:layout_below="@id/myanimelist_status_layout"
|
|
||||||
android:background="?android:attr/divider"/>
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:id="@+id/myanimelist_chapters_layout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
|
||||||
android:layout_below="@id/divider2"
|
|
||||||
android:background="?attr/selectable_list_drawable"
|
|
||||||
android:clickable="true"
|
|
||||||
android:paddingLeft="?android:listPreferredItemPaddingLeft"
|
|
||||||
android:paddingRight="?android:listPreferredItemPaddingRight">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
style="@style/TextAppearance.Regular.Body1"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:text="Chapters"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/myanimelist_chapters"
|
|
||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
tools:text="12/24"/>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/divider3"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:layout_below="@id/myanimelist_chapters_layout"
|
|
||||||
android:background="?android:attr/divider"/>
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:id="@+id/myanimelist_score_layout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
|
||||||
android:layout_below="@id/divider3"
|
|
||||||
android:background="?attr/selectable_list_drawable"
|
|
||||||
android:clickable="true"
|
|
||||||
android:paddingLeft="?android:listPreferredItemPaddingLeft"
|
|
||||||
android:paddingRight="?android:listPreferredItemPaddingRight">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
style="@style/TextAppearance.Regular.Body1"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:text="@string/score"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/myanimelist_score"
|
|
||||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
tools:text="10"/>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
</android.support.v7.widget.CardView>
|
|
@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:background="?attr/selectable_list_drawable">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/myanimelist_result_title"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="10dp"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
@ -14,11 +14,11 @@
|
|||||||
android:paddingRight="@dimen/margin_right">
|
android:paddingRight="@dimen/margin_right">
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/myanimelist_search_field"
|
android:id="@+id/track_search"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:hint="@string/title_hint"/>
|
android:hint="@string/title"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
@ -33,7 +33,7 @@
|
|||||||
android:visibility="gone"/>
|
android:visibility="gone"/>
|
||||||
|
|
||||||
<ListView
|
<ListView
|
||||||
android:id="@+id/myanimelist_search_results"
|
android:id="@+id/track_search_list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:choiceMode="singleChoice"
|
android:choiceMode="singleChoice"
|
@ -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>
|
|
20
app/src/main/res/layout/fragment_track.xml
Normal file
20
app/src/main/res/layout/fragment_track.xml
Normal 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>
|
185
app/src/main/res/layout/item_track.xml
Normal file
185
app/src/main/res/layout/item_track.xml
Normal 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>
|
14
app/src/main/res/layout/item_track_search.xml
Normal file
14
app/src/main/res/layout/item_track_search.xml
Normal 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>
|
@ -65,7 +65,7 @@
|
|||||||
<string name="pref_category_reader">Lector</string>
|
<string name="pref_category_reader">Lector</string>
|
||||||
<string name="pref_category_downloads">Descargas</string>
|
<string name="pref_category_downloads">Descargas</string>
|
||||||
<string name="pref_category_sources">Fuentes</string>
|
<string name="pref_category_sources">Fuentes</string>
|
||||||
<string name="pref_category_sync">Sincronización</string>
|
<string name="pref_category_tracking">Seguimiento</string>
|
||||||
<string name="pref_category_advanced">Avanzado</string>
|
<string name="pref_category_advanced">Avanzado</string>
|
||||||
<string name="pref_category_about">Acerca de</string>
|
<string name="pref_category_about">Acerca de</string>
|
||||||
|
|
||||||
@ -232,7 +232,7 @@
|
|||||||
<string name="on_hold">En espera</string>
|
<string name="on_hold">En espera</string>
|
||||||
<string name="plan_to_read">Para leer luego</string>
|
<string name="plan_to_read">Para leer luego</string>
|
||||||
<string name="score">Puntuación</string>
|
<string name="score">Puntuación</string>
|
||||||
<string name="title_hint">Título…</string>
|
<string name="title">Título</string>
|
||||||
<string name="status">Estado</string>
|
<string name="status">Estado</string>
|
||||||
<string name="chapters">Capítulos</string>
|
<string name="chapters">Capítulos</string>
|
||||||
|
|
||||||
|
@ -240,7 +240,7 @@
|
|||||||
<string name="on_hold">Em espera</string>
|
<string name="on_hold">Em espera</string>
|
||||||
<string name="plan_to_read">Planeada a leitura</string>
|
<string name="plan_to_read">Planeada a leitura</string>
|
||||||
<string name="score">Avaliação</string>
|
<string name="score">Avaliação</string>
|
||||||
<string name="title_hint">Título…</string>
|
<string name="title">Título</string>
|
||||||
<string name="status">Estado</string>
|
<string name="status">Estado</string>
|
||||||
<string name="chapters">Capítulos</string>
|
<string name="chapters">Capítulos</string>
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<string name="pref_category_general_key">pref_category_general_key</string>
|
<string name="pref_category_general_key">pref_category_general_key</string>
|
||||||
<string name="pref_category_reader_key">pref_category_reader_key</string>
|
<string name="pref_category_reader_key">pref_category_reader_key</string>
|
||||||
<string name="pref_category_sync_key">pref_category_sync_key</string>
|
<string name="pref_category_tracking_key">pref_category_tracking_key</string>
|
||||||
<string name="pref_category_downloads_key">pref_category_downloads_key</string>
|
<string name="pref_category_downloads_key">pref_category_downloads_key</string>
|
||||||
<string name="pref_category_advanced_key">pref_category_advanced_key</string>
|
<string name="pref_category_advanced_key">pref_category_advanced_key</string>
|
||||||
<string name="pref_category_about_key">pref_category_about_key</string>
|
<string name="pref_category_about_key">pref_category_about_key</string>
|
||||||
@ -52,7 +52,7 @@
|
|||||||
<string name="pref_last_used_category_key">last_used_category</string>
|
<string name="pref_last_used_category_key">last_used_category</string>
|
||||||
|
|
||||||
<string name="pref_source_languages">pref_source_languages</string>
|
<string name="pref_source_languages">pref_source_languages</string>
|
||||||
<string name="pref_category_manga_sync_accounts_key">category_manga_sync_accounts</string>
|
<string name="pref_category_tracking_accounts_key">category_tracking_accounts</string>
|
||||||
|
|
||||||
<string name="pref_clear_chapter_cache_key">pref_clear_chapter_cache_key</string>
|
<string name="pref_clear_chapter_cache_key">pref_clear_chapter_cache_key</string>
|
||||||
<string name="pref_clear_database_key">pref_clear_database_key</string>
|
<string name="pref_clear_database_key">pref_clear_database_key</string>
|
||||||
|
@ -80,7 +80,7 @@
|
|||||||
<string name="pref_category_reader">Reader</string>
|
<string name="pref_category_reader">Reader</string>
|
||||||
<string name="pref_category_downloads">Downloads</string>
|
<string name="pref_category_downloads">Downloads</string>
|
||||||
<string name="pref_category_sources">Sources</string>
|
<string name="pref_category_sources">Sources</string>
|
||||||
<string name="pref_category_sync">Sync</string>
|
<string name="pref_category_tracking">Tracking</string>
|
||||||
<string name="pref_category_advanced">Advanced</string>
|
<string name="pref_category_advanced">Advanced</string>
|
||||||
<string name="pref_category_about">About</string>
|
<string name="pref_category_about">About</string>
|
||||||
|
|
||||||
@ -276,13 +276,14 @@
|
|||||||
<string name="confirm_delete_chapters">Are you sure you want to delete selected chapters?</string>
|
<string name="confirm_delete_chapters">Are you sure you want to delete selected chapters?</string>
|
||||||
|
|
||||||
<!-- MyAnimeList fragment -->
|
<!-- MyAnimeList fragment -->
|
||||||
|
<string name="manga_tracking_tab">Tracking</string>
|
||||||
<string name="reading">Reading</string>
|
<string name="reading">Reading</string>
|
||||||
<string name="completed">Completed</string>
|
<string name="completed">Completed</string>
|
||||||
<string name="dropped">Dropped</string>
|
<string name="dropped">Dropped</string>
|
||||||
<string name="on_hold">On hold</string>
|
<string name="on_hold">On hold</string>
|
||||||
<string name="plan_to_read">Plan to read</string>
|
<string name="plan_to_read">Plan to read</string>
|
||||||
<string name="score">Score</string>
|
<string name="score">Score</string>
|
||||||
<string name="title_hint">Title…</string>
|
<string name="title">Title</string>
|
||||||
<string name="status">Status</string>
|
<string name="status">Status</string>
|
||||||
<string name="chapters">Chapters</string>
|
<string name="chapters">Chapters</string>
|
||||||
|
|
||||||
|
@ -1,30 +1,32 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<PreferenceScreen
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
<PreferenceScreen
|
<PreferenceScreen
|
||||||
android:icon="@drawable/ic_sync_black_24dp"
|
android:icon="@drawable/ic_sync_black_24dp"
|
||||||
android:key="sync_screen"
|
android:key="tracking_screen"
|
||||||
android:persistent="false"
|
android:persistent="false"
|
||||||
android:title="@string/pref_category_sync"
|
android:title="@string/pref_category_tracking"
|
||||||
app:asp_tintEnabled="true">
|
app:asp_tintEnabled="true">
|
||||||
|
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
android:key="@string/pref_auto_update_manga_sync_key"
|
android:key="@string/pref_auto_update_manga_sync_key"
|
||||||
android:title="@string/pref_auto_update_manga_sync"
|
android:title="@string/pref_auto_update_manga_sync"
|
||||||
android:defaultValue="true" />
|
android:defaultValue="true"
|
||||||
|
app:showText="false"/>
|
||||||
|
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
android:key="@string/pref_ask_update_manga_sync_key"
|
android:key="@string/pref_ask_update_manga_sync_key"
|
||||||
android:title="@string/pref_ask_update_manga_sync"
|
android:title="@string/pref_ask_update_manga_sync"
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
android:dependency="@string/pref_auto_update_manga_sync_key" />
|
android:dependency="@string/pref_auto_update_manga_sync_key"
|
||||||
|
app:showText="false"/>
|
||||||
|
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
android:key="@string/pref_category_manga_sync_accounts_key"
|
android:key="@string/pref_category_tracking_accounts_key"
|
||||||
android:title="@string/services"
|
android:title="@string/services"
|
||||||
android:persistent="false" />
|
android:persistent="false"
|
||||||
|
app:showText="false"/>
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
|
@ -390,16 +390,16 @@ class BackupTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testRestoreSyncForManga() {
|
fun testRestoreSyncForManga() {
|
||||||
// Create a manga and mangaSync
|
// Create a manga and track
|
||||||
val manga = createManga("title")
|
val manga = createManga("title")
|
||||||
manga.id = 1L
|
manga.id = 1L
|
||||||
|
|
||||||
val mangaSync = createMangaSync(manga, 1, 2, 3)
|
val track = createTrack(manga, 1, 2, 3)
|
||||||
|
|
||||||
// Add an entry for the manga
|
// Add an entry for the manga
|
||||||
val entry = JsonObject()
|
val entry = JsonObject()
|
||||||
entry.add("manga", toJson(manga))
|
entry.add("manga", toJson(manga))
|
||||||
entry.add("sync", toJson(mangaSync))
|
entry.add("sync", toJson(track))
|
||||||
|
|
||||||
// Append the entry to the backup list
|
// Append the entry to the backup list
|
||||||
val mangas = ArrayList<JsonElement>()
|
val mangas = ArrayList<JsonElement>()
|
||||||
@ -412,7 +412,7 @@ class BackupTest {
|
|||||||
val dbManga = db.getManga(1).executeAsBlocking()
|
val dbManga = db.getManga(1).executeAsBlocking()
|
||||||
assertThat(dbManga).isNotNull()
|
assertThat(dbManga).isNotNull()
|
||||||
|
|
||||||
val dbSync = db.getMangasSync(dbManga!!).executeAsBlocking()
|
val dbSync = db.getTracks(dbManga!!).executeAsBlocking()
|
||||||
assertThat(dbSync).hasSize(3)
|
assertThat(dbSync).hasSize(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,13 +422,13 @@ class BackupTest {
|
|||||||
// Create a manga and 3 sync
|
// Create a manga and 3 sync
|
||||||
val manga = createManga("title")
|
val manga = createManga("title")
|
||||||
manga.id = mangaId
|
manga.id = mangaId
|
||||||
val mangaSync = createMangaSync(manga, 1, 2, 3)
|
val track = createTrack(manga, 1, 2, 3)
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
|
||||||
// Add an entry for the manga
|
// Add an entry for the manga
|
||||||
val entry = JsonObject()
|
val entry = JsonObject()
|
||||||
entry.add("manga", toJson(manga))
|
entry.add("manga", toJson(manga))
|
||||||
entry.add("sync", toJson(mangaSync))
|
entry.add("sync", toJson(track))
|
||||||
|
|
||||||
// Append the entry to the backup list
|
// Append the entry to the backup list
|
||||||
val mangas = ArrayList<JsonElement>()
|
val mangas = ArrayList<JsonElement>()
|
||||||
@ -441,7 +441,7 @@ class BackupTest {
|
|||||||
val dbManga = db.getManga(mangaId).executeAsBlocking()
|
val dbManga = db.getManga(mangaId).executeAsBlocking()
|
||||||
assertThat(dbManga).isNotNull()
|
assertThat(dbManga).isNotNull()
|
||||||
|
|
||||||
val dbSync = db.getMangasSync(dbManga!!).executeAsBlocking()
|
val dbSync = db.getTracks(dbManga!!).executeAsBlocking()
|
||||||
assertThat(dbSync).hasSize(3)
|
assertThat(dbSync).hasSize(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -451,17 +451,17 @@ class BackupTest {
|
|||||||
// Store a manga and 3 sync
|
// Store a manga and 3 sync
|
||||||
val manga = createManga("title")
|
val manga = createManga("title")
|
||||||
manga.id = mangaId
|
manga.id = mangaId
|
||||||
var mangaSync = createMangaSync(manga, 1, 2, 3)
|
var track = createTrack(manga, 1, 2, 3)
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
db.insertMangasSync(mangaSync).executeAsBlocking()
|
db.insertTracks(track).executeAsBlocking()
|
||||||
|
|
||||||
// The backup contains a existing sync and a new one, so it should have 4 sync
|
// The backup contains a existing sync and a new one, so it should have 4 sync
|
||||||
mangaSync = createMangaSync(manga, 3, 4)
|
track = createTrack(manga, 3, 4)
|
||||||
|
|
||||||
// Add an entry for the manga
|
// Add an entry for the manga
|
||||||
val entry = JsonObject()
|
val entry = JsonObject()
|
||||||
entry.add("manga", toJson(manga))
|
entry.add("manga", toJson(manga))
|
||||||
entry.add("sync", toJson(mangaSync))
|
entry.add("sync", toJson(track))
|
||||||
|
|
||||||
// Append the entry to the backup list
|
// Append the entry to the backup list
|
||||||
val mangas = ArrayList<JsonElement>()
|
val mangas = ArrayList<JsonElement>()
|
||||||
@ -474,7 +474,7 @@ class BackupTest {
|
|||||||
val dbManga = db.getManga(mangaId).executeAsBlocking()
|
val dbManga = db.getManga(mangaId).executeAsBlocking()
|
||||||
assertThat(dbManga).isNotNull()
|
assertThat(dbManga).isNotNull()
|
||||||
|
|
||||||
val dbSync = db.getMangasSync(dbManga!!).executeAsBlocking()
|
val dbSync = db.getTracks(dbManga!!).executeAsBlocking()
|
||||||
assertThat(dbSync).hasSize(4)
|
assertThat(dbSync).hasSize(4)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -546,17 +546,17 @@ class BackupTest {
|
|||||||
return chapters
|
return chapters
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createMangaSync(manga: Manga, syncId: Int): MangaSync {
|
private fun createTrack(manga: Manga, syncId: Int): Track {
|
||||||
val m = MangaSync.create(syncId)
|
val m = Track.create(syncId)
|
||||||
m.manga_id = manga.id!!
|
m.manga_id = manga.id!!
|
||||||
m.title = "title"
|
m.title = "title"
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createMangaSync(manga: Manga, vararg syncIds: Int): List<MangaSync> {
|
private fun createTrack(manga: Manga, vararg syncIds: Int): List<Track> {
|
||||||
val ms = ArrayList<MangaSync>()
|
val ms = ArrayList<Track>()
|
||||||
for (title in syncIds) {
|
for (title in syncIds) {
|
||||||
ms.add(createMangaSync(manga, title))
|
ms.add(createTrack(manga, title))
|
||||||
}
|
}
|
||||||
return ms
|
return ms
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user