mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 19:17:51 +02:00
Compare commits
28 Commits
Author | SHA1 | Date | |
---|---|---|---|
9bcde69ee0 | |||
beca2b429c | |||
3a1699f0b3 | |||
a7192e866f | |||
dc882b4dce | |||
77b4de3941 | |||
006d17aac7 | |||
1a3a1db4ff | |||
97fa659283 | |||
f1d84ccb49 | |||
1f14240251 | |||
ea6fed6ecf | |||
d09eca7833 | |||
2c6f64c5ae | |||
ec87e4359b | |||
2b63bae989 | |||
82f4e3157a | |||
725ceab00b | |||
ba428c401d | |||
510669ee2c | |||
8d749df290 | |||
091c0c0c71 | |||
7fdd2cacd7 | |||
2241a0b2de | |||
d21a93123b | |||
e542a8d8e2 | |||
94ee4e7fb5 | |||
e3d430eb5e |
12
.travis.yml
12
.travis.yml
@ -12,11 +12,21 @@ android:
|
||||
- extra-android-support
|
||||
- extra-google-google_play_services
|
||||
|
||||
licenses:
|
||||
- android-sdk-license-.+
|
||||
- '.+'
|
||||
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
|
||||
before_script:
|
||||
- chmod +x gradlew
|
||||
- chmod +x gradlew
|
||||
|
||||
before_install:
|
||||
- mkdir "$ANDROID_HOME/licenses" || true
|
||||
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license"
|
||||
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
|
||||
|
||||
#Build, and run tests
|
||||
script: "./gradlew clean buildStandardDebug"
|
||||
sudo: false
|
||||
|
@ -38,8 +38,8 @@ android {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 25
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
versionCode 17
|
||||
versionName "0.4.1"
|
||||
versionCode 18
|
||||
versionName "0.4.2"
|
||||
|
||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||
@ -98,7 +98,7 @@ android {
|
||||
dependencies {
|
||||
|
||||
// Modified dependencies
|
||||
compile 'com.github.inorichi:subsampling-scale-image-view:f687b74'
|
||||
compile 'com.github.inorichi:subsampling-scale-image-view:c4db85c'
|
||||
|
||||
// Android support library
|
||||
final support_library_version = '25.0.1'
|
||||
@ -110,14 +110,16 @@ dependencies {
|
||||
compile "com.android.support:support-annotations:$support_library_version"
|
||||
compile "com.android.support:customtabs:$support_library_version"
|
||||
|
||||
compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4'
|
||||
|
||||
compile 'com.android.support:multidex:1.0.1'
|
||||
|
||||
// ReactiveX
|
||||
compile 'io.reactivex:rxandroid:1.2.1'
|
||||
compile 'io.reactivex:rxjava:1.2.3'
|
||||
compile 'io.reactivex:rxjava:1.2.4'
|
||||
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||
compile 'com.github.pwittchen:reactivenetwork:0.6.0'
|
||||
compile 'com.github.pwittchen:reactivenetwork:0.7.0'
|
||||
|
||||
// Network client
|
||||
compile "com.squareup.okhttp3:okhttp:3.5.0"
|
||||
@ -172,7 +174,7 @@ dependencies {
|
||||
compile 'jp.wasabeef:glide-transformations:2.0.1'
|
||||
|
||||
// Logging
|
||||
compile 'com.jakewharton.timber:timber:4.3.1'
|
||||
compile 'com.jakewharton.timber:timber:4.4.0'
|
||||
|
||||
// Crash reports
|
||||
compile 'ch.acra:acra:4.9.1'
|
||||
@ -202,7 +204,7 @@ dependencies {
|
||||
}
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.0.5-2'
|
||||
ext.kotlin_version = '1.0.6'
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
@ -53,6 +53,18 @@
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/FilePickerTheme">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.setting.AnilistLoginActivity"
|
||||
android:label="Anilist">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:host="anilist-auth"
|
||||
android:scheme="tachiyomi" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
@ -70,7 +82,7 @@
|
||||
<service android:name=".data.download.DownloadService"
|
||||
android:exported="false"/>
|
||||
|
||||
<service android:name=".data.mangasync.UpdateMangaSyncService"
|
||||
<service android:name=".data.track.TrackUpdateService"
|
||||
android:exported="false"/>
|
||||
|
||||
<service android:name=".data.updater.UpdateDownloaderService"
|
||||
|
@ -20,7 +20,7 @@ import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
||||
reportType = org.acra.sender.HttpSender.Type.JSON,
|
||||
httpMethod = org.acra.sender.HttpSender.Method.PUT,
|
||||
buildConfigClass = BuildConfig::class,
|
||||
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*")
|
||||
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*", ".*token.*")
|
||||
)
|
||||
open class App : Application() {
|
||||
|
||||
@ -34,7 +34,7 @@ open class App : Application() {
|
||||
setupAcra()
|
||||
setupJobManager()
|
||||
|
||||
LocaleHelper.updateCfg(this, baseContext.resources.configuration)
|
||||
LocaleHelper.updateConfiguration(this, resources.configuration)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
@ -46,7 +46,7 @@ open class App : Application() {
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
LocaleHelper.updateCfg(this, newConfig)
|
||||
LocaleHelper.updateConfiguration(this, newConfig, true)
|
||||
}
|
||||
|
||||
protected open fun setupAcra() {
|
||||
|
@ -6,7 +6,7 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
@ -32,7 +32,7 @@ class AppModule(val app: Application) : InjektModule {
|
||||
|
||||
addSingletonFactory { DownloadManager(app) }
|
||||
|
||||
addSingletonFactory { MangaSyncManager(app) }
|
||||
addSingletonFactory { TrackManager(app) }
|
||||
|
||||
addSingletonFactory { Gson() }
|
||||
|
||||
|
@ -39,7 +39,7 @@ class BackupManager(private val db: DatabaseHelper) {
|
||||
private val MANGA = "manga"
|
||||
private val MANGAS = "mangas"
|
||||
private val CHAPTERS = "chapters"
|
||||
private val MANGA_SYNC = "sync"
|
||||
private val TRACK = "sync"
|
||||
private val CATEGORIES = "categories"
|
||||
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
@ -109,10 +109,10 @@ class BackupManager(private val db: DatabaseHelper) {
|
||||
entry.add(CHAPTERS, gson.toJsonTree(chapters))
|
||||
}
|
||||
|
||||
// Backup manga sync
|
||||
val mangaSync = db.getMangasSync(manga).executeAsBlocking()
|
||||
if (!mangaSync.isEmpty()) {
|
||||
entry.add(MANGA_SYNC, gson.toJsonTree(mangaSync))
|
||||
// Backup tracks
|
||||
val tracks = db.getTracks(manga).executeAsBlocking()
|
||||
if (!tracks.isEmpty()) {
|
||||
entry.add(TRACK, gson.toJsonTree(tracks))
|
||||
}
|
||||
|
||||
// Backup categories for this manga
|
||||
@ -231,13 +231,13 @@ class BackupManager(private val db: DatabaseHelper) {
|
||||
val element = backupManga.asJsonObject
|
||||
val manga = gson.fromJson(element.get(MANGA), MangaImpl::class.java)
|
||||
val chapters = gson.fromJson<List<ChapterImpl>>(element.get(CHAPTERS) ?: JsonArray())
|
||||
val sync = gson.fromJson<List<MangaSyncImpl>>(element.get(MANGA_SYNC) ?: JsonArray())
|
||||
val tracks = gson.fromJson<List<TrackImpl>>(element.get(TRACK) ?: JsonArray())
|
||||
val categories = gson.fromJson<List<String>>(element.get(CATEGORIES) ?: JsonArray())
|
||||
|
||||
// Restore everything related to this manga
|
||||
restoreManga(manga)
|
||||
restoreChaptersForManga(manga, chapters)
|
||||
restoreSyncForManga(manga, sync)
|
||||
restoreSyncForManga(manga, tracks)
|
||||
restoreCategoriesForManga(manga, categories)
|
||||
}
|
||||
}
|
||||
@ -333,35 +333,35 @@ class BackupManager(private val db: DatabaseHelper) {
|
||||
* Restores the sync of a manga.
|
||||
*
|
||||
* @param manga the manga whose sync have to be restored.
|
||||
* @param sync the sync to restore.
|
||||
* @param tracks the track list to restore.
|
||||
*/
|
||||
private fun restoreSyncForManga(manga: Manga, sync: List<MangaSync>) {
|
||||
private fun restoreSyncForManga(manga: Manga, tracks: List<Track>) {
|
||||
// Fix foreign keys with the current manga id
|
||||
for (mangaSync in sync) {
|
||||
mangaSync.manga_id = manga.id!!
|
||||
for (track in tracks) {
|
||||
track.manga_id = manga.id!!
|
||||
}
|
||||
|
||||
val dbSyncs = db.getMangasSync(manga).executeAsBlocking()
|
||||
val syncToUpdate = ArrayList<MangaSync>()
|
||||
for (backupSync in sync) {
|
||||
val dbTracks = db.getTracks(manga).executeAsBlocking()
|
||||
val trackToUpdate = ArrayList<Track>()
|
||||
for (backupTrack in tracks) {
|
||||
// Try to find existing chapter in db
|
||||
val pos = dbSyncs.indexOf(backupSync)
|
||||
val pos = dbTracks.indexOf(backupTrack)
|
||||
if (pos != -1) {
|
||||
// The sync is already in the db, only update its fields
|
||||
val dbSync = dbSyncs[pos]
|
||||
val dbSync = dbTracks[pos]
|
||||
// Mark the max chapter as read and nothing else
|
||||
dbSync.last_chapter_read = Math.max(backupSync.last_chapter_read, dbSync.last_chapter_read)
|
||||
syncToUpdate.add(dbSync)
|
||||
dbSync.last_chapter_read = Math.max(backupTrack.last_chapter_read, dbSync.last_chapter_read)
|
||||
trackToUpdate.add(dbSync)
|
||||
} else {
|
||||
// Insert new sync. Let the db assign the id
|
||||
backupSync.id = null
|
||||
syncToUpdate.add(backupSync)
|
||||
backupTrack.id = null
|
||||
trackToUpdate.add(backupTrack)
|
||||
}
|
||||
}
|
||||
|
||||
// Update database
|
||||
if (!syncToUpdate.isEmpty()) {
|
||||
db.insertMangasSync(syncToUpdate).executeAsBlocking()
|
||||
if (!trackToUpdate.isEmpty()) {
|
||||
db.insertTracks(trackToUpdate).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ import com.google.gson.FieldAttributes
|
||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSyncImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||
|
||||
class IdExclusion : ExclusionStrategy {
|
||||
|
||||
@ -17,7 +17,7 @@ class IdExclusion : ExclusionStrategy {
|
||||
override fun shouldSkipField(f: FieldAttributes) = when (f.declaringClass) {
|
||||
MangaImpl::class.java -> mangaExclusions.contains(f.name)
|
||||
ChapterImpl::class.java -> chapterExclusions.contains(f.name)
|
||||
MangaSyncImpl::class.java -> syncExclusions.contains(f.name)
|
||||
TrackImpl::class.java -> syncExclusions.contains(f.name)
|
||||
CategoryImpl::class.java -> categoryExclusions.contains(f.name)
|
||||
else -> false
|
||||
}
|
||||
|
@ -10,13 +10,13 @@ import eu.kanade.tachiyomi.data.database.queries.*
|
||||
* This class provides operations to manage the database through its interfaces.
|
||||
*/
|
||||
open class DatabaseHelper(context: Context)
|
||||
: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
||||
: MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
||||
|
||||
override val db = DefaultStorIOSQLite.builder()
|
||||
.sqliteOpenHelper(DbOpenHelper(context))
|
||||
.addTypeMapping(Manga::class.java, MangaTypeMapping())
|
||||
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
|
||||
.addTypeMapping(MangaSync::class.java, MangaSyncTypeMapping())
|
||||
.addTypeMapping(Track::class.java, TrackTypeMapping())
|
||||
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
||||
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
||||
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
||||
|
@ -23,7 +23,7 @@ class DbOpenHelper(context: Context)
|
||||
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
||||
execSQL(MangaTable.createTableQuery)
|
||||
execSQL(ChapterTable.createTableQuery)
|
||||
execSQL(MangaSyncTable.createTableQuery)
|
||||
execSQL(TrackTable.createTableQuery)
|
||||
execSQL(CategoryTable.createTableQuery)
|
||||
execSQL(MangaCategoryTable.createTableQuery)
|
||||
execSQL(HistoryTable.createTableQuery)
|
||||
|
@ -9,38 +9,38 @@ import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSyncImpl
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_LAST_CHAPTER_READ
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_MANGA_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_REMOTE_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_SCORE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_STATUS
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_SYNC_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_TITLE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_TOTAL_CHAPTERS
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.TABLE
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_REMOTE_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TOTAL_CHAPTERS
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
|
||||
|
||||
class MangaSyncTypeMapping : SQLiteTypeMapping<MangaSync>(
|
||||
MangaSyncPutResolver(),
|
||||
MangaSyncGetResolver(),
|
||||
MangaSyncDeleteResolver()
|
||||
class TrackTypeMapping : SQLiteTypeMapping<Track>(
|
||||
TrackPutResolver(),
|
||||
TrackGetResolver(),
|
||||
TrackDeleteResolver()
|
||||
)
|
||||
|
||||
class MangaSyncPutResolver : DefaultPutResolver<MangaSync>() {
|
||||
class TrackPutResolver : DefaultPutResolver<Track>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: MangaSync) = InsertQuery.builder()
|
||||
override fun mapToInsertQuery(obj: Track) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: MangaSync) = UpdateQuery.builder()
|
||||
override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: MangaSync) = ContentValues(9).apply {
|
||||
override fun mapToContentValues(obj: Track) = ContentValues(9).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_MANGA_ID, obj.manga_id)
|
||||
put(COL_SYNC_ID, obj.sync_id)
|
||||
@ -53,9 +53,9 @@ class MangaSyncPutResolver : DefaultPutResolver<MangaSync>() {
|
||||
}
|
||||
}
|
||||
|
||||
class MangaSyncGetResolver : DefaultGetResolver<MangaSync>() {
|
||||
class TrackGetResolver : DefaultGetResolver<Track>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): MangaSync = MangaSyncImpl().apply {
|
||||
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
|
||||
@ -68,9 +68,9 @@ class MangaSyncGetResolver : DefaultGetResolver<MangaSync>() {
|
||||
}
|
||||
}
|
||||
|
||||
class MangaSyncDeleteResolver : DefaultDeleteResolver<MangaSync>() {
|
||||
class TrackDeleteResolver : DefaultDeleteResolver<Track>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: MangaSync) = DeleteQuery.builder()
|
||||
override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
interface MangaSync : Serializable {
|
||||
interface Track : Serializable {
|
||||
|
||||
var id: Long?
|
||||
|
||||
@ -24,7 +24,7 @@ interface MangaSync : Serializable {
|
||||
|
||||
var update: Boolean
|
||||
|
||||
fun copyPersonalFrom(other: MangaSync) {
|
||||
fun copyPersonalFrom(other: Track) {
|
||||
last_chapter_read = other.last_chapter_read
|
||||
score = other.score
|
||||
status = other.status
|
||||
@ -32,7 +32,7 @@ interface MangaSync : Serializable {
|
||||
|
||||
companion object {
|
||||
|
||||
fun create(serviceId: Int): MangaSync = MangaSyncImpl().apply {
|
||||
fun create(serviceId: Int): Track = TrackImpl().apply {
|
||||
sync_id = serviceId
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
class MangaSyncImpl : MangaSync {
|
||||
class TrackImpl : Track {
|
||||
|
||||
override var id: Long? = null
|
||||
|
||||
@ -26,11 +26,11 @@ class MangaSyncImpl : MangaSync {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val mangaSync = other as MangaSync
|
||||
other as Track
|
||||
|
||||
if (manga_id != mangaSync.manga_id) return false
|
||||
if (sync_id != mangaSync.sync_id) return false
|
||||
return remote_id == mangaSync.remote_id
|
||||
if (manga_id != other.manga_id) return false
|
||||
if (sync_id != other.sync_id) return false
|
||||
return remote_id == other.remote_id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
@ -1,46 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
||||
|
||||
interface MangaSyncQueries : DbProvider {
|
||||
|
||||
fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get()
|
||||
.`object`(MangaSync::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaSyncTable.TABLE)
|
||||
.where("${MangaSyncTable.COL_MANGA_ID} = ? AND " +
|
||||
"${MangaSyncTable.COL_SYNC_ID} = ?")
|
||||
.whereArgs(manga.id, sync.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getMangasSync(manga: Manga) = db.get()
|
||||
.listOfObjects(MangaSync::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaSyncTable.TABLE)
|
||||
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare()
|
||||
|
||||
fun insertMangasSync(mangas: List<MangaSync>) = db.put().objects(mangas).prepare()
|
||||
|
||||
fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
|
||||
|
||||
fun deleteMangaSyncForManga(manga: Manga) = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(MangaSyncTable.TABLE)
|
||||
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
|
||||
interface TrackQueries : DbProvider {
|
||||
|
||||
fun getTracks(manga: Manga) = db.get()
|
||||
.listOfObjects(Track::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(TrackTable.TABLE)
|
||||
.where("${TrackTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun insertTrack(track: Track) = db.put().`object`(track).prepare()
|
||||
|
||||
fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare()
|
||||
|
||||
fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(TrackTable.TABLE)
|
||||
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
|
||||
.whereArgs(manga.id, sync.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables
|
||||
|
||||
object MangaSyncTable {
|
||||
object TrackTable {
|
||||
|
||||
const val TABLE = "manga_sync"
|
||||
|
@ -1,23 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.data.mangasync.myanimelist.MyAnimeList
|
||||
|
||||
class MangaSyncManager(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
const val MYANIMELIST = 1
|
||||
const val ANILIST = 2
|
||||
}
|
||||
|
||||
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
||||
|
||||
val aniList = Anilist(context, ANILIST)
|
||||
|
||||
// TODO enable anilist
|
||||
val services = listOf(myAnimeList)
|
||||
|
||||
fun getService(id: Int) = services.find { it.id == id }
|
||||
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync
|
||||
|
||||
import android.content.Context
|
||||
import android.support.annotation.CallSuper
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import okhttp3.OkHttpClient
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class MangaSyncService(private val context: Context, val id: Int) {
|
||||
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
val networkService: NetworkHelper by injectLazy()
|
||||
|
||||
open val client: OkHttpClient
|
||||
get() = networkService.client
|
||||
|
||||
// Name of the manga sync service to display
|
||||
abstract val name: String
|
||||
|
||||
abstract fun login(username: String, password: String): Completable
|
||||
|
||||
open val isLogged: Boolean
|
||||
get() = !getUsername().isEmpty() &&
|
||||
!getPassword().isEmpty()
|
||||
|
||||
abstract fun add(manga: MangaSync): Observable<MangaSync>
|
||||
|
||||
abstract fun update(manga: MangaSync): Observable<MangaSync>
|
||||
|
||||
abstract fun bind(manga: MangaSync): Observable<MangaSync>
|
||||
|
||||
abstract fun getStatus(status: Int): String
|
||||
|
||||
fun saveCredentials(username: String, password: String) {
|
||||
preferences.setMangaSyncCredentials(this, username, password)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
open fun logout() {
|
||||
preferences.setMangaSyncCredentials(this, "", "")
|
||||
}
|
||||
|
||||
fun getUsername() = preferences.mangaSyncUsername(this)
|
||||
|
||||
fun getPassword() = preferences.mangaSyncPassword(this)
|
||||
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.anilist
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
|
||||
class Anilist(private val context: Context, id: Int) : MangaSyncService(context, id) {
|
||||
|
||||
companion object {
|
||||
const val READING = 1
|
||||
const val COMPLETED = 2
|
||||
const val ON_HOLD = 3
|
||||
const val DROPPED = 4
|
||||
const val PLAN_TO_READ = 5
|
||||
|
||||
const val DEFAULT_STATUS = READING
|
||||
const val DEFAULT_SCORE = 0
|
||||
}
|
||||
|
||||
override val name = "AniList"
|
||||
|
||||
private val interceptor by lazy { AnilistInterceptor(getPassword()) }
|
||||
|
||||
private val api by lazy {
|
||||
AnilistApi.createService(networkService.client.newBuilder()
|
||||
.addInterceptor(interceptor)
|
||||
.build())
|
||||
}
|
||||
|
||||
override fun login(username: String, password: String) = login(password)
|
||||
|
||||
fun login(authCode: String): Completable {
|
||||
// Create a new api with the default client to avoid request interceptions.
|
||||
return AnilistApi.createService(client)
|
||||
// Request the access token from the API with the authorization code.
|
||||
.requestAccessToken(authCode)
|
||||
// Save the token in the interceptor.
|
||||
.doOnNext { interceptor.setAuth(it) }
|
||||
// Obtain the authenticated user from the API.
|
||||
.zipWith(api.getCurrentUser().map { it["id"].toString() })
|
||||
{ oauth, user -> Pair(user, oauth.refresh_token!!) }
|
||||
// Save service credentials (username and refresh token).
|
||||
.doOnNext { saveCredentials(it.first, it.second) }
|
||||
// Logout on any error.
|
||||
.doOnError { logout() }
|
||||
.toCompletable()
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
super.logout()
|
||||
interceptor.setAuth(null)
|
||||
}
|
||||
|
||||
fun search(query: String): Observable<List<MangaSync>> {
|
||||
return api.search(query, 1)
|
||||
.flatMap { Observable.from(it) }
|
||||
.filter { it.type != "Novel" }
|
||||
.map { it.toMangaSync() }
|
||||
.toList()
|
||||
}
|
||||
|
||||
fun getList(): Observable<List<MangaSync>> {
|
||||
return api.getList(getUsername())
|
||||
.flatMap { Observable.from(it.flatten()) }
|
||||
.map { it.toMangaSync() }
|
||||
.toList()
|
||||
}
|
||||
|
||||
override fun add(manga: MangaSync): Observable<MangaSync> {
|
||||
return api.addManga(manga.remote_id, manga.last_chapter_read, manga.getAnilistStatus(),
|
||||
manga.score.toInt())
|
||||
.doOnNext { it.body().close() }
|
||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
|
||||
.doOnError { Timber.e(it, it.message) }
|
||||
.map { manga }
|
||||
}
|
||||
|
||||
override fun update(manga: MangaSync): Observable<MangaSync> {
|
||||
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
|
||||
manga.status = COMPLETED
|
||||
}
|
||||
return api.updateManga(manga.remote_id, manga.last_chapter_read, manga.getAnilistStatus(),
|
||||
manga.score.toInt())
|
||||
.doOnNext { it.body().close() }
|
||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
|
||||
.doOnError { Timber.e(it, it.message) }
|
||||
.map { manga }
|
||||
}
|
||||
|
||||
override fun bind(manga: MangaSync): Observable<MangaSync> {
|
||||
return getList()
|
||||
.flatMap { userlist ->
|
||||
manga.sync_id = id
|
||||
val mangaFromList = userlist.find { it.remote_id == manga.remote_id }
|
||||
if (mangaFromList != null) {
|
||||
manga.copyPersonalFrom(mangaFromList)
|
||||
update(manga)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
manga.score = DEFAULT_SCORE.toFloat()
|
||||
manga.status = DEFAULT_STATUS
|
||||
add(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getStatus(status: Int): String = with(context) {
|
||||
when (status) {
|
||||
READING -> getString(R.string.reading)
|
||||
COMPLETED -> getString(R.string.completed)
|
||||
ON_HOLD -> getString(R.string.on_hold)
|
||||
DROPPED -> getString(R.string.dropped)
|
||||
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun MangaSync.getAnilistStatus() = when (status) {
|
||||
READING -> "reading"
|
||||
COMPLETED -> "completed"
|
||||
ON_HOLD -> "on-hold"
|
||||
DROPPED -> "dropped"
|
||||
PLAN_TO_READ -> "plan to read"
|
||||
else -> throw NotImplementedError("Unknown status")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,89 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.anilist
|
||||
|
||||
import android.net.Uri
|
||||
import com.google.gson.JsonObject
|
||||
import eu.kanade.tachiyomi.data.mangasync.anilist.model.ALManga
|
||||
import eu.kanade.tachiyomi.data.mangasync.anilist.model.ALUserLists
|
||||
import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.*
|
||||
import rx.Observable
|
||||
|
||||
interface AnilistApi {
|
||||
|
||||
companion object {
|
||||
private const val clientId = "tachiyomi-hrtje"
|
||||
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
|
||||
private const val clientUrl = "tachiyomi://anilist-auth"
|
||||
private const val baseUrl = "https://anilist.co/api/"
|
||||
|
||||
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon()
|
||||
.appendQueryParameter("grant_type", "authorization_code")
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("redirect_uri", clientUrl)
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.build()
|
||||
|
||||
fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token",
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("refresh_token", token)
|
||||
.build())
|
||||
|
||||
fun createService(client: OkHttpClient) = Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||
.build()
|
||||
.create(AnilistApi::class.java)
|
||||
|
||||
}
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("auth/access_token")
|
||||
fun requestAccessToken(
|
||||
@Field("code") code: String,
|
||||
@Field("grant_type") grant_type: String = "authorization_code",
|
||||
@Field("client_id") client_id: String = clientId,
|
||||
@Field("client_secret") client_secret: String = clientSecret,
|
||||
@Field("redirect_uri") redirect_uri: String = clientUrl)
|
||||
: Observable<OAuth>
|
||||
|
||||
@GET("user")
|
||||
fun getCurrentUser(): Observable<JsonObject>
|
||||
|
||||
@GET("manga/search/{query}")
|
||||
fun search(@Path("query") query: String, @Query("page") page: Int): Observable<List<ALManga>>
|
||||
|
||||
@GET("user/{username}/mangalist")
|
||||
fun getList(@Path("username") username: String): Observable<ALUserLists>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("mangalist")
|
||||
fun addManga(
|
||||
@Field("id") id: Int,
|
||||
@Field("chapters_read") chapters_read: Int,
|
||||
@Field("list_status") list_status: String,
|
||||
@Field("score_raw") score_raw: Int)
|
||||
: Observable<Response<ResponseBody>>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("mangalist")
|
||||
fun updateManga(
|
||||
@Field("id") id: Int,
|
||||
@Field("chapters_read") chapters_read: Int,
|
||||
@Field("list_status") list_status: String,
|
||||
@Field("score_raw") score_raw: Int)
|
||||
: Observable<Response<ResponseBody>>
|
||||
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||
|
||||
data class ALManga(
|
||||
val id: Int,
|
||||
val title_romaji: String,
|
||||
val type: String,
|
||||
val total_chapters: Int) {
|
||||
|
||||
fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply {
|
||||
remote_id = this@ALManga.id
|
||||
title = title_romaji
|
||||
total_chapters = this@ALManga.total_chapters
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
||||
|
||||
data class ALUserLists(val lists: Map<String, List<ALUserManga>>) {
|
||||
|
||||
fun flatten() = lists.values.flatten()
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||
import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist
|
||||
|
||||
data class ALUserManga(
|
||||
val id: Int,
|
||||
val list_status: String,
|
||||
val score_raw: Int,
|
||||
val chapters_read: Int,
|
||||
val manga: ALManga) {
|
||||
|
||||
fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply {
|
||||
remote_id = manga.id
|
||||
status = getMangaSyncStatus()
|
||||
score = score_raw.toFloat()
|
||||
last_chapter_read = chapters_read
|
||||
}
|
||||
|
||||
fun getMangaSyncStatus() = when (list_status) {
|
||||
"reading" -> Anilist.READING
|
||||
"completed" -> Anilist.COMPLETED
|
||||
"on-hold" -> Anilist.ON_HOLD
|
||||
"dropped" -> Anilist.DROPPED
|
||||
"plan to read" -> Anilist.PLAN_TO_READ
|
||||
else -> throw NotImplementedError("Unknown status")
|
||||
}
|
||||
}
|
@ -1,222 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.myanimelist
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Xml
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
||||
import eu.kanade.tachiyomi.data.network.GET
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import eu.kanade.tachiyomi.data.network.asObservable
|
||||
import eu.kanade.tachiyomi.util.selectInt
|
||||
import eu.kanade.tachiyomi.util.selectText
|
||||
import okhttp3.Credentials
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.RequestBody
|
||||
import org.jsoup.Jsoup
|
||||
import org.xmlpull.v1.XmlSerializer
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import java.io.StringWriter
|
||||
|
||||
class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) {
|
||||
|
||||
private lateinit var headers: Headers
|
||||
|
||||
companion object {
|
||||
val BASE_URL = "https://myanimelist.net"
|
||||
|
||||
private val ENTRY_TAG = "entry"
|
||||
private val CHAPTER_TAG = "chapter"
|
||||
private val SCORE_TAG = "score"
|
||||
private val STATUS_TAG = "status"
|
||||
|
||||
val READING = 1
|
||||
val COMPLETED = 2
|
||||
val ON_HOLD = 3
|
||||
val DROPPED = 4
|
||||
val PLAN_TO_READ = 6
|
||||
|
||||
val DEFAULT_STATUS = READING
|
||||
val DEFAULT_SCORE = 0
|
||||
}
|
||||
|
||||
init {
|
||||
val username = getUsername()
|
||||
val password = getPassword()
|
||||
|
||||
if (!username.isEmpty() && !password.isEmpty()) {
|
||||
createHeaders(username, password)
|
||||
}
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "MyAnimeList"
|
||||
|
||||
fun getLoginUrl() = Uri.parse(BASE_URL).buildUpon()
|
||||
.appendEncodedPath("api/account/verify_credentials.xml")
|
||||
.toString()
|
||||
|
||||
fun getSearchUrl(query: String) = Uri.parse(BASE_URL).buildUpon()
|
||||
.appendEncodedPath("api/manga/search.xml")
|
||||
.appendQueryParameter("q", query)
|
||||
.toString()
|
||||
|
||||
fun getListUrl(username: String) = Uri.parse(BASE_URL).buildUpon()
|
||||
.appendPath("malappinfo.php")
|
||||
.appendQueryParameter("u", username)
|
||||
.appendQueryParameter("status", "all")
|
||||
.appendQueryParameter("type", "manga")
|
||||
.toString()
|
||||
|
||||
fun getUpdateUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
|
||||
.appendEncodedPath("api/mangalist/update")
|
||||
.appendPath("${manga.remote_id}.xml")
|
||||
.toString()
|
||||
|
||||
fun getAddUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
|
||||
.appendEncodedPath("api/mangalist/add")
|
||||
.appendPath("${manga.remote_id}.xml")
|
||||
.toString()
|
||||
|
||||
override fun login(username: String, password: String): Completable {
|
||||
createHeaders(username, password)
|
||||
return client.newCall(GET(getLoginUrl(), headers))
|
||||
.asObservable()
|
||||
.doOnNext { it.close() }
|
||||
.doOnNext { if (it.code() != 200) throw Exception("Login error") }
|
||||
.toCompletable()
|
||||
}
|
||||
|
||||
fun search(query: String): Observable<List<MangaSync>> {
|
||||
return client.newCall(GET(getSearchUrl(query), headers))
|
||||
.asObservable()
|
||||
.map { Jsoup.parse(it.body().string()) }
|
||||
.flatMap { Observable.from(it.select("entry")) }
|
||||
.filter { it.select("type").text() != "Novel" }
|
||||
.map {
|
||||
MangaSync.create(id).apply {
|
||||
title = it.selectText("title")!!
|
||||
remote_id = it.selectInt("id")
|
||||
total_chapters = it.selectInt("chapters")
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
// MAL doesn't support score with decimals
|
||||
fun getList(): Observable<List<MangaSync>> {
|
||||
return networkService.forceCacheClient
|
||||
.newCall(GET(getListUrl(getUsername()), headers))
|
||||
.asObservable()
|
||||
.map { Jsoup.parse(it.body().string()) }
|
||||
.flatMap { Observable.from(it.select("manga")) }
|
||||
.map {
|
||||
MangaSync.create(id).apply {
|
||||
title = it.selectText("series_title")!!
|
||||
remote_id = it.selectInt("series_mangadb_id")
|
||||
last_chapter_read = it.selectInt("my_read_chapters")
|
||||
status = it.selectInt("my_status")
|
||||
score = it.selectInt("my_score").toFloat()
|
||||
total_chapters = it.selectInt("series_chapters")
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
override fun update(manga: MangaSync): Observable<MangaSync> {
|
||||
return Observable.defer {
|
||||
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
|
||||
manga.status = COMPLETED
|
||||
}
|
||||
client.newCall(POST(getUpdateUrl(manga), headers, getMangaPostPayload(manga)))
|
||||
.asObservable()
|
||||
.doOnNext { it.close() }
|
||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
|
||||
.map { manga }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun add(manga: MangaSync): Observable<MangaSync> {
|
||||
return Observable.defer {
|
||||
client.newCall(POST(getAddUrl(manga), headers, getMangaPostPayload(manga)))
|
||||
.asObservable()
|
||||
.doOnNext { it.close() }
|
||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
|
||||
.map { manga }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMangaPostPayload(manga: MangaSync): RequestBody {
|
||||
val xml = Xml.newSerializer()
|
||||
val writer = StringWriter()
|
||||
|
||||
with(xml) {
|
||||
setOutput(writer)
|
||||
startDocument("UTF-8", false)
|
||||
startTag("", ENTRY_TAG)
|
||||
|
||||
// Last chapter read
|
||||
if (manga.last_chapter_read != 0) {
|
||||
inTag(CHAPTER_TAG, manga.last_chapter_read.toString())
|
||||
}
|
||||
// Manga status in the list
|
||||
inTag(STATUS_TAG, manga.status.toString())
|
||||
|
||||
// Manga score
|
||||
inTag(SCORE_TAG, manga.score.toString())
|
||||
|
||||
endTag("", ENTRY_TAG)
|
||||
endDocument()
|
||||
}
|
||||
|
||||
val form = FormBody.Builder()
|
||||
form.add("data", writer.toString())
|
||||
return form.build()
|
||||
}
|
||||
|
||||
fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
|
||||
startTag(namespace, tag)
|
||||
text(body)
|
||||
endTag(namespace, tag)
|
||||
}
|
||||
|
||||
override fun bind(manga: MangaSync): Observable<MangaSync> {
|
||||
return getList()
|
||||
.flatMap { userlist ->
|
||||
manga.sync_id = id
|
||||
val mangaFromList = userlist.find { it.remote_id == manga.remote_id }
|
||||
if (mangaFromList != null) {
|
||||
manga.copyPersonalFrom(mangaFromList)
|
||||
update(manga)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
manga.score = DEFAULT_SCORE.toFloat()
|
||||
manga.status = DEFAULT_STATUS
|
||||
add(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getStatus(status: Int): String = with(context) {
|
||||
when (status) {
|
||||
READING -> getString(R.string.reading)
|
||||
COMPLETED -> getString(R.string.completed)
|
||||
ON_HOLD -> getString(R.string.on_hold)
|
||||
DROPPED -> getString(R.string.dropped)
|
||||
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
fun createHeaders(username: String, password: String) {
|
||||
val builder = Headers.Builder()
|
||||
builder.add("Authorization", Credentials.basic(username, password))
|
||||
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
|
||||
headers = builder.build()
|
||||
}
|
||||
|
||||
}
|
@ -50,7 +50,7 @@ fun Call.asObservableSuccess(): Observable<Response> {
|
||||
return asObservable().doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw Exception("Unsuccessful code ${response.code()}")
|
||||
throw Exception("HTTP error ${response.code()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,9 +51,9 @@ class PreferenceKeys(context: Context) {
|
||||
|
||||
val updateOnlyNonCompleted = context.getString(R.string.pref_update_only_non_completed_key)
|
||||
|
||||
val autoUpdateMangaSync = context.getString(R.string.pref_auto_update_manga_sync_key)
|
||||
val autoUpdateTrack = context.getString(R.string.pref_auto_update_manga_sync_key)
|
||||
|
||||
val askUpdateMangaSync = context.getString(R.string.pref_ask_update_manga_sync_key)
|
||||
val askUpdateTrack = context.getString(R.string.pref_ask_update_manga_sync_key)
|
||||
|
||||
val lastUsedCatalogueSource = context.getString(R.string.pref_last_catalogue_source_key)
|
||||
|
||||
@ -95,9 +95,11 @@ class PreferenceKeys(context: Context) {
|
||||
|
||||
fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId"
|
||||
|
||||
fun syncUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||
|
||||
fun syncPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||
|
||||
fun trackToken(syncId: Int) = "track_token_$syncId"
|
||||
|
||||
val libraryAsList = context.getString(R.string.pref_display_library_as_list)
|
||||
|
||||
|
@ -7,15 +7,15 @@ import android.preference.PreferenceManager
|
||||
import com.f2prateek.rx.preferences.Preference
|
||||
import com.f2prateek.rx.preferences.RxSharedPreferences
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
||||
import eu.kanade.tachiyomi.data.source.Source
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import java.io.File
|
||||
|
||||
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
|
||||
|
||||
fun Preference<Boolean>.invert(): Boolean = getOrDefault().let { set(!it); !it }
|
||||
|
||||
class PreferencesHelper(context: Context) {
|
||||
class PreferencesHelper(val context: Context) {
|
||||
|
||||
val keys = PreferenceKeys(context)
|
||||
|
||||
@ -70,9 +70,9 @@ class PreferencesHelper(context: Context) {
|
||||
|
||||
fun updateOnlyNonCompleted() = prefs.getBoolean(keys.updateOnlyNonCompleted, false)
|
||||
|
||||
fun autoUpdateMangaSync() = prefs.getBoolean(keys.autoUpdateMangaSync, true)
|
||||
fun autoUpdateTrack() = prefs.getBoolean(keys.autoUpdateTrack, true)
|
||||
|
||||
fun askUpdateMangaSync() = prefs.getBoolean(keys.askUpdateMangaSync, false)
|
||||
fun askUpdateTrack() = prefs.getBoolean(keys.askUpdateTrack, false)
|
||||
|
||||
fun lastUsedCatalogueSource() = rxPrefs.getInteger(keys.lastUsedCatalogueSource, -1)
|
||||
|
||||
@ -82,7 +82,7 @@ class PreferencesHelper(context: Context) {
|
||||
|
||||
fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
|
||||
|
||||
fun enabledLanguages() = rxPrefs.getStringSet(keys.enabledLanguages, setOf("EN"))
|
||||
fun enabledLanguages() = rxPrefs.getStringSet(keys.enabledLanguages, setOf("en"))
|
||||
|
||||
fun sourceUsername(source: Source) = prefs.getString(keys.sourceUsername(source.id), "")
|
||||
|
||||
@ -95,17 +95,21 @@ class PreferencesHelper(context: Context) {
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun mangaSyncUsername(sync: MangaSyncService) = prefs.getString(keys.syncUsername(sync.id), "")
|
||||
fun trackUsername(sync: TrackService) = prefs.getString(keys.trackUsername(sync.id), "")
|
||||
|
||||
fun mangaSyncPassword(sync: MangaSyncService) = prefs.getString(keys.syncPassword(sync.id), "")
|
||||
fun trackPassword(sync: TrackService) = prefs.getString(keys.trackPassword(sync.id), "")
|
||||
|
||||
fun setMangaSyncCredentials(sync: MangaSyncService, username: String, password: String) {
|
||||
fun setTrackCredentials(sync: TrackService, username: String, password: String) {
|
||||
prefs.edit()
|
||||
.putString(keys.syncUsername(sync.id), username)
|
||||
.putString(keys.syncPassword(sync.id), password)
|
||||
.putString(keys.trackUsername(sync.id), username)
|
||||
.putString(keys.trackPassword(sync.id), password)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun trackToken(sync: TrackService) = rxPrefs.getString(keys.trackToken(sync.id), "")
|
||||
|
||||
fun anilistScoreType() = rxPrefs.getInteger("anilist_score_type", 0)
|
||||
|
||||
fun downloadsDirectory() = rxPrefs.getString(keys.downloadsDirectory, defaultDownloadsDir.toString())
|
||||
|
||||
fun downloadThreads() = rxPrefs.getInteger(keys.downloadThreads, 1)
|
||||
@ -138,6 +142,6 @@ class PreferencesHelper(context: Context) {
|
||||
|
||||
fun downloadNew() = prefs.getBoolean(keys.downloadNew, false)
|
||||
|
||||
fun lang() = prefs.getInt(keys.lang, 0)
|
||||
fun lang() = prefs.getString(keys.lang, "")
|
||||
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.source
|
||||
|
||||
class Language(val code: String, val lang: String)
|
||||
|
||||
val DE = Language("DE", "German")
|
||||
val EN = Language("EN", "English")
|
||||
val RU = Language("RU", "Russian")
|
||||
|
||||
fun getLanguages() = listOf(DE, EN, RU)
|
@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.data.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.data.network.newCallWithProgress
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.source.Language
|
||||
import eu.kanade.tachiyomi.data.source.Source
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
@ -47,9 +46,9 @@ abstract class OnlineSource() : Source {
|
||||
abstract val baseUrl: String
|
||||
|
||||
/**
|
||||
* Language of the source.
|
||||
* An ISO 639-1 compliant language code (two characters in lower case).
|
||||
*/
|
||||
abstract val lang: Language
|
||||
abstract val lang: String
|
||||
|
||||
/**
|
||||
* Whether the source has support for latest updates.
|
||||
@ -82,7 +81,7 @@ abstract class OnlineSource() : Source {
|
||||
/**
|
||||
* Visible name of the source.
|
||||
*/
|
||||
override fun toString() = "$name (${lang.code})"
|
||||
override fun toString() = "$name (${lang.toUpperCase()})"
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||
|
@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.network.GET
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import eu.kanade.tachiyomi.data.source.getLanguages
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
@ -27,9 +26,7 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
|
||||
if (it.endsWith("/")) it.dropLast(1) else it
|
||||
}
|
||||
|
||||
override val lang = map.lang.toUpperCase().let { code ->
|
||||
getLanguages().find { code == it.code }!!
|
||||
}
|
||||
override val lang = map.lang.toLowerCase()
|
||||
|
||||
override val supportsLatest = map.latestupdates != null
|
||||
|
||||
@ -39,7 +36,7 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
|
||||
}
|
||||
|
||||
override val id = map.id.let {
|
||||
if (it is Int) it else (lang.code.hashCode() + 31 * it.hashCode()) and 0x7fffffff
|
||||
if (it is Int) it else (lang.toUpperCase().hashCode() + 31 * it.hashCode()) and 0x7fffffff
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: MangasPage): Request {
|
||||
|
@ -7,8 +7,6 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.network.GET
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import eu.kanade.tachiyomi.data.network.asObservable
|
||||
import eu.kanade.tachiyomi.data.source.EN
|
||||
import eu.kanade.tachiyomi.data.source.Language
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.data.source.online.LoginSource
|
||||
@ -33,7 +31,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
||||
|
||||
override val baseUrl = "http://bato.to"
|
||||
|
||||
override val lang: Language get() = EN
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
@ -109,12 +107,17 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "#show_more_row"
|
||||
|
||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&order_cond=views&order=desc&p=1&genre_cond=and&genres=${getFilterParams(filters)}"
|
||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&order_cond=views&order=desc&p=1${getFilterParams(filters)}"
|
||||
|
||||
private fun getFilterParams(filters: List<Filter>): String = filters
|
||||
.map {
|
||||
";i" + it.id
|
||||
}.joinToString()
|
||||
private fun getFilterParams(filters: List<Filter>): String {
|
||||
var genres = ""
|
||||
var completed = ""
|
||||
for (filter in filters) {
|
||||
if (filter.equals(completedFilter)) completed = "&completed=c"
|
||||
else genres += ";i" + filter.id
|
||||
}
|
||||
return if (genres.isEmpty()) completed else "&genres=$genres&genre_cond=and$completed"
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
|
||||
if (page.page == 1) {
|
||||
@ -133,7 +136,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
||||
}
|
||||
|
||||
page.nextPageUrl = document.select(searchMangaNextPageSelector()).first()?.let {
|
||||
"$baseUrl/search_ajax?name=${Uri.encode(query)}&p=${page.page + 1}&order_cond=views&order=desc&genre_cond=and&genres=" + getFilterParams(filters)
|
||||
"$baseUrl/search_ajax?name=${Uri.encode(query)}&order_cond=views&order=desc&p=${page.page + 1}${getFilterParams(filters)}"
|
||||
}
|
||||
}
|
||||
|
||||
@ -301,11 +304,13 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
||||
}
|
||||
}
|
||||
|
||||
private val completedFilter = Filter("completed", "Completed")
|
||||
// [...document.querySelectorAll("#advanced_options div.genre_buttons")].map((el,i) => {
|
||||
// const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Filter("${id}", "${el.textContent.trim()}")`
|
||||
// }).join(',\n')
|
||||
// on https://bato.to/search
|
||||
override fun getFilterList(): List<Filter> = listOf(
|
||||
completedFilter,
|
||||
Filter("40", "4-Koma"),
|
||||
Filter("1", "Action"),
|
||||
Filter("2", "Adventure"),
|
||||
|
@ -1,12 +1,9 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.english
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.network.GET
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import eu.kanade.tachiyomi.data.source.EN
|
||||
import eu.kanade.tachiyomi.data.source.Language
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
@ -25,7 +22,7 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override val baseUrl = "http://kissmanga.com"
|
||||
|
||||
override val lang: Language get() = EN
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
@ -62,10 +59,10 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
|
||||
val form = FormBody.Builder().apply {
|
||||
add("authorArtist", "")
|
||||
add("mangaName", query)
|
||||
add("status", "")
|
||||
|
||||
this@Kissmanga.filters.forEach { filter ->
|
||||
add("genres", if (filter in filters) "1" else "0")
|
||||
if (filter.equals(completedFilter)) add("status", if (filter in filters) filter.id else "")
|
||||
else add("genres", if (filter in filters) "1" else "0")
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,9 +128,11 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
private val completedFilter = Filter("Completed", "Completed")
|
||||
// $("select[name=\"genres\"]").map((i,el) => `Filter("${i}", "${$(el).next().text().trim()}")`).get().join(',\n')
|
||||
// on http://kissmanga.com/AdvanceSearch
|
||||
override fun getFilterList(): List<Filter> = listOf(
|
||||
completedFilter,
|
||||
Filter("0", "Action"),
|
||||
Filter("1", "Adult"),
|
||||
Filter("2", "Adventure"),
|
||||
|
@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.data.source.online.english
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.source.EN
|
||||
import eu.kanade.tachiyomi.data.source.Language
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
@ -20,7 +18,7 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override val baseUrl = "http://mangafox.me"
|
||||
|
||||
override val lang: Language get() = EN
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
@ -50,10 +48,10 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
|
||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
|
||||
"$baseUrl/search.php?name_method=cw&advopts=1&order=za&sort=views&name=$query&page=1&${filters.map { it.id + "=1" }.joinToString("&")}"
|
||||
|
||||
override fun searchMangaSelector() = "table#listing > tbody > tr:gt(0)"
|
||||
override fun searchMangaSelector() = "div#mangalist > ul.list > li"
|
||||
|
||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||
element.select("a.series_preview").first().let {
|
||||
element.select("a.title").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
@ -132,6 +130,7 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
|
||||
// $('select.genres').map((i,el)=>`Filter("${$(el).attr('name')}", "${$(el).next().text().trim()}")`).get().join(',\n')
|
||||
// on http://kissmanga.com/AdvanceSearch
|
||||
override fun getFilterList(): List<Filter> = listOf(
|
||||
Filter("is_completed", "Completed"),
|
||||
Filter("genres[Action]", "Action"),
|
||||
Filter("genres[Adult]", "Adult"),
|
||||
Filter("genres[Adventure]", "Adventure"),
|
||||
|
@ -1,10 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.english
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.source.EN
|
||||
import eu.kanade.tachiyomi.data.source.Language
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
import org.jsoup.nodes.Document
|
||||
@ -19,7 +16,7 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override val baseUrl = "http://www.mangahere.co"
|
||||
|
||||
override val lang: Language get() = EN
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
@ -31,13 +28,17 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override fun latestUpdatesSelector() = "div.directory_list > ul > li"
|
||||
|
||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||
element.select("div.title > a").first().let {
|
||||
private fun mangaFromElement(query: String, element: Element, manga: Manga) {
|
||||
element.select(query).first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
manga.title = if (it.hasAttr("title")) it.attr("title") else if (it.hasAttr("rel")) it.attr("rel") else it.text()
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||
mangaFromElement("div.title > a", element, manga)
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
||||
popularMangaFromElement(element, manga)
|
||||
}
|
||||
@ -51,10 +52,7 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
|
||||
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
|
||||
|
||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||
element.select("a.manga_info").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
mangaFromElement("a.manga_info", element, manga)
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
|
||||
@ -136,6 +134,7 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
|
||||
// [...document.querySelectorAll("select[id^='genres'")].map((el,i) => `Filter("${el.getAttribute('name')}", "${el.nextSibling.nextSibling.textContent.trim()}")`).join(',\n')
|
||||
// http://www.mangahere.co/advsearch.htm
|
||||
override fun getFilterList(): List<Filter> = listOf(
|
||||
Filter("is_completed", "Completed"),
|
||||
Filter("genres[Action]", "Action"),
|
||||
Filter("genres[Adventure]", "Adventure"),
|
||||
Filter("genres[Comedy]", "Comedy"),
|
||||
|
@ -3,8 +3,6 @@ package eu.kanade.tachiyomi.data.source.online.english
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import eu.kanade.tachiyomi.data.source.EN
|
||||
import eu.kanade.tachiyomi.data.source.Language
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
@ -24,7 +22,7 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override val baseUrl = "http://mangaseeonline.net"
|
||||
|
||||
override val lang: Language get() = EN
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
@ -66,8 +64,16 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
|
||||
// Not used, overrides parent.
|
||||
override fun popularMangaNextPageSelector() = ""
|
||||
|
||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
|
||||
"$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending&keyword=$query&genre=${filters.map { it.id }.joinToString(",")}"
|
||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>): String {
|
||||
var url = "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending&keyword=$query"
|
||||
var genres: String? = null
|
||||
for (filter in filters) {
|
||||
if (filter.equals(completedFilter)) url += "&status=Complete"
|
||||
else if (genres == null) genres = filter.id
|
||||
else genres += "," + filter.id
|
||||
}
|
||||
return if (genres == null) url else url + "&genre=$genres"
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div.searchResults > div.requested > div.row"
|
||||
|
||||
@ -168,9 +174,11 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override fun imageUrlParse(document: Document): String = document.select("img.CurImage").attr("src")
|
||||
|
||||
private val completedFilter = Filter("Complete", "Completed")
|
||||
// [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
|
||||
// http://mangasee.co/advanced-search/
|
||||
override fun getFilterList(): List<Filter> = listOf(
|
||||
completedFilter,
|
||||
Filter("Action", "Action"),
|
||||
Filter("Adult", "Adult"),
|
||||
Filter("Adventure", "Adventure"),
|
||||
@ -250,4 +258,4 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,11 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.english
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import eu.kanade.tachiyomi.data.source.EN
|
||||
import eu.kanade.tachiyomi.data.source.Language
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||
@ -23,7 +20,7 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override val baseUrl = "http://www.readmanga.today"
|
||||
|
||||
override val lang: Language get() = EN
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
@ -72,10 +69,12 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
|
||||
val builder = okhttp3.FormBody.Builder()
|
||||
builder.add("manga-name", query)
|
||||
builder.add("type", "all")
|
||||
builder.add("status", "both")
|
||||
var status = "both"
|
||||
for (filter in filters) {
|
||||
builder.add("include[]", filter.id)
|
||||
if (filter.equals(completedFilter)) status = filter.id
|
||||
else builder.add("include[]", filter.id)
|
||||
}
|
||||
builder.add("status", status)
|
||||
|
||||
return POST(page.url, headers, builder.build())
|
||||
}
|
||||
@ -154,9 +153,11 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src")
|
||||
|
||||
private val completedFilter = Filter("completed", "Completed")
|
||||
// [...document.querySelectorAll("ul.manga-cat span")].map(el => `Filter("${el.getAttribute('data-id')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
|
||||
// http://www.readmanga.today/advanced-search
|
||||
override fun getFilterList(): List<Filter> = listOf(
|
||||
completedFilter,
|
||||
Filter("2", "Action"),
|
||||
Filter("4", "Adventure"),
|
||||
Filter("5", "Comedy"),
|
||||
|
@ -1,10 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.german
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.source.DE
|
||||
import eu.kanade.tachiyomi.data.source.Language
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
@ -19,7 +16,7 @@ class WieManga(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override val baseUrl = "http://www.wiemanga.com"
|
||||
|
||||
override val lang: Language get() = DE
|
||||
override val lang = "de"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
|
@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.data.source.online.russian
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.source.Language
|
||||
import eu.kanade.tachiyomi.data.source.RU
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
@ -20,7 +18,7 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override val baseUrl = "http://mangachan.me"
|
||||
|
||||
override val lang: Language get() = RU
|
||||
override val lang = "ru"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
|
@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.data.source.online.russian
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.source.Language
|
||||
import eu.kanade.tachiyomi.data.source.RU
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
import okhttp3.Response
|
||||
@ -19,7 +17,7 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override val baseUrl = "http://mintmanga.com"
|
||||
|
||||
override val lang: Language get() = RU
|
||||
override val lang = "ru"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
|
@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.data.source.online.russian
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.source.Language
|
||||
import eu.kanade.tachiyomi.data.source.RU
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
import okhttp3.Response
|
||||
@ -19,7 +17,7 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override val baseUrl = "http://readmanga.me"
|
||||
|
||||
override val lang: Language get() = RU
|
||||
override val lang = "ru"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
|
@ -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,70 @@
|
||||
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
|
||||
|
||||
@DrawableRes
|
||||
abstract fun getLogo(): Int
|
||||
|
||||
abstract fun getLogoColor(): Int
|
||||
|
||||
abstract fun getStatusList(): List<Int>
|
||||
|
||||
abstract fun getStatus(status: Int): String
|
||||
|
||||
abstract fun getScoreList(): List<String>
|
||||
|
||||
open fun indexToScore(index: Int): Float {
|
||||
return index.toFloat()
|
||||
}
|
||||
|
||||
abstract fun displayScore(track: Track): String
|
||||
|
||||
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 login(username: String, password: String): Completable
|
||||
|
||||
@CallSuper
|
||||
open fun logout() {
|
||||
preferences.setTrackCredentials(this, "", "")
|
||||
}
|
||||
|
||||
open val isLogged: Boolean
|
||||
get() = !getUsername().isEmpty() &&
|
||||
!getPassword().isEmpty()
|
||||
|
||||
fun getUsername() = preferences.trackUsername(this)
|
||||
|
||||
fun getPassword() = preferences.trackPassword(this)
|
||||
|
||||
fun saveCredentials(username: String, password: String) {
|
||||
preferences.setTrackCredentials(this, username, password)
|
||||
}
|
||||
|
||||
}
|
@ -1,20 +1,20 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync
|
||||
package eu.kanade.tachiyomi.data.track
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class UpdateMangaSyncService : Service() {
|
||||
class TrackUpdateService : Service() {
|
||||
|
||||
val syncManager: MangaSyncManager by injectLazy()
|
||||
val trackManager: TrackManager by injectLazy()
|
||||
val db: DatabaseHelper by injectLazy()
|
||||
|
||||
private lateinit var subscriptions: CompositeSubscription
|
||||
@ -30,9 +30,9 @@ class UpdateMangaSyncService : Service() {
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
val manga = intent.getSerializableExtra(EXTRA_MANGASYNC)
|
||||
if (manga != null) {
|
||||
updateLastChapterRead(manga as MangaSync, startId)
|
||||
val track = intent.getSerializableExtra(EXTRA_TRACK)
|
||||
if (track != null) {
|
||||
updateLastChapterRead(track as Track, startId)
|
||||
return Service.START_REDELIVER_INTENT
|
||||
} else {
|
||||
stopSelf(startId)
|
||||
@ -44,15 +44,15 @@ class UpdateMangaSyncService : Service() {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun updateLastChapterRead(mangaSync: MangaSync, startId: Int) {
|
||||
val sync = syncManager.getService(mangaSync.sync_id)
|
||||
private fun updateLastChapterRead(track: Track, startId: Int) {
|
||||
val sync = trackManager.getService(track.sync_id)
|
||||
if (sync == null) {
|
||||
stopSelf(startId)
|
||||
return
|
||||
}
|
||||
|
||||
subscriptions.add(Observable.defer { sync.update(mangaSync) }
|
||||
.flatMap { db.insertMangaSync(mangaSync).asRxObservable() }
|
||||
subscriptions.add(Observable.defer { sync.update(track) }
|
||||
.flatMap { db.insertTrack(track).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ stopSelf(startId) },
|
||||
@ -61,12 +61,12 @@ class UpdateMangaSyncService : Service() {
|
||||
|
||||
companion object {
|
||||
|
||||
private val EXTRA_MANGASYNC = "extra_mangasync"
|
||||
private val EXTRA_TRACK = "extra_track"
|
||||
|
||||
@JvmStatic
|
||||
fun start(context: Context, mangaSync: MangaSync) {
|
||||
val intent = Intent(context, UpdateMangaSyncService::class.java)
|
||||
intent.putExtra(EXTRA_MANGASYNC, mangaSync)
|
||||
fun start(context: Context, track: Track) {
|
||||
val intent = Intent(context, TrackUpdateService::class.java)
|
||||
intent.putExtra(EXTRA_TRACK, track)
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
package eu.kanade.tachiyomi.data.track.anilist
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
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
|
||||
|
||||
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(client, interceptor) }
|
||||
|
||||
override fun getLogo() = R.drawable.al
|
||||
|
||||
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
||||
|
||||
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 -> ""
|
||||
}
|
||||
}
|
||||
|
||||
override fun getScoreList(): List<String> {
|
||||
return when (preferences.anilistScoreType().getOrDefault()) {
|
||||
// 10 point
|
||||
0 -> IntRange(0, 10).map(Int::toString)
|
||||
// 100 point
|
||||
1 -> IntRange(0, 100).map(Int::toString)
|
||||
// 5 stars
|
||||
2 -> IntRange(0, 5).map { "$it ★" }
|
||||
// Smiley
|
||||
3 -> listOf("-", "😦", "😐", "😊")
|
||||
// 10 point decimal
|
||||
4 -> IntRange(0, 100).map { (it / 10f).toString() }
|
||||
else -> throw Exception("Unknown score type")
|
||||
}
|
||||
}
|
||||
|
||||
override fun indexToScore(index: Int): Float {
|
||||
return when (preferences.anilistScoreType().getOrDefault()) {
|
||||
// 10 point
|
||||
0 -> index * 10f
|
||||
// 100 point
|
||||
1 -> index.toFloat()
|
||||
// 5 stars
|
||||
2 -> index * 20f
|
||||
// Smiley
|
||||
3 -> index * 30f
|
||||
// 10 point decimal
|
||||
4 -> index.toFloat()
|
||||
else -> throw Exception("Unknown score type")
|
||||
}
|
||||
}
|
||||
|
||||
override fun displayScore(track: Track): String {
|
||||
val score = track.score
|
||||
return when (preferences.anilistScoreType().getOrDefault()) {
|
||||
2 -> "${(score / 20).toInt()} ★"
|
||||
3 -> when {
|
||||
score == 0f -> "0"
|
||||
score <= 30 -> "😦"
|
||||
score <= 60 -> "😐"
|
||||
else -> "😊"
|
||||
}
|
||||
else -> track.toAnilistScore()
|
||||
}
|
||||
}
|
||||
|
||||
override fun add(track: Track): Observable<Track> {
|
||||
return api.addLibManga(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.updateLibManga(track)
|
||||
}
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
return api.findLibManga(track, getUsername())
|
||||
.flatMap { remoteTrack ->
|
||||
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 search(query: String): Observable<List<Track>> {
|
||||
return api.search(query)
|
||||
}
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
return api.getLibManga(track, getUsername())
|
||||
.map { remoteTrack ->
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
override fun login(username: String, password: String) = login(password)
|
||||
|
||||
fun login(authCode: String): Completable {
|
||||
return api.login(authCode)
|
||||
// Save the token in the interceptor.
|
||||
.doOnNext { interceptor.setAuth(it) }
|
||||
// Obtain the authenticated user from the API.
|
||||
.zipWith(api.getCurrentUser().map { pair ->
|
||||
preferences.anilistScoreType().set(pair.second)
|
||||
pair.first
|
||||
}, { 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,161 @@
|
||||
package eu.kanade.tachiyomi.data.track.anilist
|
||||
|
||||
import android.net.Uri
|
||||
import com.github.salomonbrys.kotson.int
|
||||
import com.github.salomonbrys.kotson.string
|
||||
import com.google.gson.JsonObject
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.*
|
||||
import rx.Observable
|
||||
|
||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
private val rest = restBuilder()
|
||||
.client(client.newBuilder().addInterceptor(interceptor).build())
|
||||
.build()
|
||||
.create(Rest::class.java)
|
||||
|
||||
fun addLibManga(track: Track): Observable<Track> {
|
||||
return rest.addLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus())
|
||||
.map { response ->
|
||||
response.body().close()
|
||||
if (!response.isSuccessful) {
|
||||
throw Exception("Could not add manga")
|
||||
}
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track): Observable<Track> {
|
||||
return rest.updateLibManga(track.remote_id, track.last_chapter_read, track.toAnilistStatus(),
|
||||
track.toAnilistScore())
|
||||
.map { response ->
|
||||
response.body().close()
|
||||
if (!response.isSuccessful) {
|
||||
throw Exception("Could not update manga")
|
||||
}
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
fun search(query: String): Observable<List<Track>> {
|
||||
return rest.search(query, 1)
|
||||
.map { list ->
|
||||
list.filter { it.type != "Novel" }.map { it.toTrack() }
|
||||
}
|
||||
.onErrorReturn { emptyList() }
|
||||
}
|
||||
|
||||
fun getList(username: String): Observable<List<Track>> {
|
||||
return rest.getLib(username)
|
||||
.map { lib ->
|
||||
lib.flatten().map { it.toTrack() }
|
||||
}
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track, username: String) : Observable<Track?> {
|
||||
// TODO avoid getting the entire list
|
||||
return getList(username)
|
||||
.map { list -> list.find { it.remote_id == track.remote_id } }
|
||||
}
|
||||
|
||||
fun getLibManga(track: Track, username: String): Observable<Track> {
|
||||
return findLibManga(track, username)
|
||||
.map { it ?: throw Exception("Could not find manga") }
|
||||
}
|
||||
|
||||
fun login(authCode: String): Observable<OAuth> {
|
||||
return restBuilder()
|
||||
.client(client)
|
||||
.build()
|
||||
.create(Rest::class.java)
|
||||
.requestAccessToken(authCode)
|
||||
}
|
||||
|
||||
fun getCurrentUser(): Observable<Pair<String, Int>> {
|
||||
return rest.getCurrentUser()
|
||||
.map { it["id"].string to it["score_type"].int }
|
||||
}
|
||||
|
||||
private fun restBuilder() = Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||
|
||||
private interface Rest {
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("auth/access_token")
|
||||
fun requestAccessToken(
|
||||
@Field("code") code: String,
|
||||
@Field("grant_type") grant_type: String = "authorization_code",
|
||||
@Field("client_id") client_id: String = clientId,
|
||||
@Field("client_secret") client_secret: String = clientSecret,
|
||||
@Field("redirect_uri") redirect_uri: String = clientUrl
|
||||
) : Observable<OAuth>
|
||||
|
||||
@GET("user")
|
||||
fun getCurrentUser(): Observable<JsonObject>
|
||||
|
||||
@GET("manga/search/{query}")
|
||||
fun search(
|
||||
@Path("query") query: String,
|
||||
@Query("page") page: Int
|
||||
): Observable<List<ALManga>>
|
||||
|
||||
@GET("user/{username}/mangalist")
|
||||
fun getLib(
|
||||
@Path("username") username: String
|
||||
): Observable<ALUserLists>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("mangalist")
|
||||
fun addLibManga(
|
||||
@Field("id") id: Int,
|
||||
@Field("chapters_read") chapters_read: Int,
|
||||
@Field("list_status") list_status: String
|
||||
) : Observable<Response<ResponseBody>>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("mangalist")
|
||||
fun updateLibManga(
|
||||
@Field("id") id: Int,
|
||||
@Field("chapters_read") chapters_read: Int,
|
||||
@Field("list_status") list_status: String,
|
||||
@Field("score") score_raw: String
|
||||
) : Observable<Response<ResponseBody>>
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val clientId = "tachiyomi-hrtje"
|
||||
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
|
||||
private const val clientUrl = "tachiyomi://anilist-auth"
|
||||
private const val baseUrl = "https://anilist.co/api/"
|
||||
|
||||
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon()
|
||||
.appendQueryParameter("grant_type", "authorization_code")
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("redirect_uri", clientUrl)
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.build()
|
||||
|
||||
fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token",
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("refresh_token", token)
|
||||
.build())
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,61 +1,60 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.anilist
|
||||
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
||||
|
||||
/**
|
||||
* OAuth object used for authenticated requests.
|
||||
*
|
||||
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
|
||||
* before its original expiration date.
|
||||
*/
|
||||
private var oauth: OAuth? = null
|
||||
set(value) {
|
||||
field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
if (refreshToken.isNullOrEmpty()) {
|
||||
throw Exception("Not authenticated with Anilist")
|
||||
}
|
||||
|
||||
// Refresh access token if null or expired.
|
||||
if (oauth == null || oauth!!.isExpired()) {
|
||||
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
|
||||
oauth = if (response.isSuccessful) {
|
||||
Gson().fromJson(response.body().string(), OAuth::class.java)
|
||||
} else {
|
||||
response.close()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Throw on null auth.
|
||||
if (oauth == null) {
|
||||
throw Exception("Access token wasn't refreshed")
|
||||
}
|
||||
|
||||
// Add the authorization header to the original request.
|
||||
val authRequest = originalRequest.newBuilder()
|
||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||
.build()
|
||||
|
||||
return chain.proceed(authRequest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user authenticates with Anilist for the first time. Sets the refresh token
|
||||
* and the oauth object.
|
||||
*/
|
||||
fun setAuth(oauth: OAuth?) {
|
||||
refreshToken = oauth?.refresh_token
|
||||
this.oauth = oauth
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.data.track.anilist
|
||||
|
||||
import com.google.gson.Gson
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
||||
|
||||
/**
|
||||
* OAuth object used for authenticated requests.
|
||||
*
|
||||
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
|
||||
* before its original expiration date.
|
||||
*/
|
||||
private var oauth: OAuth? = null
|
||||
set(value) {
|
||||
field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
if (refreshToken.isNullOrEmpty()) {
|
||||
throw Exception("Not authenticated with Anilist")
|
||||
}
|
||||
|
||||
// Refresh access token if null or expired.
|
||||
if (oauth == null || oauth!!.isExpired()) {
|
||||
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
|
||||
oauth = if (response.isSuccessful) {
|
||||
Gson().fromJson(response.body().string(), OAuth::class.java)
|
||||
} else {
|
||||
response.close()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Throw on null auth.
|
||||
if (oauth == null) {
|
||||
throw Exception("Access token wasn't refreshed")
|
||||
}
|
||||
|
||||
// Add the authorization header to the original request.
|
||||
val authRequest = originalRequest.newBuilder()
|
||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||
.build()
|
||||
|
||||
return chain.proceed(authRequest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user authenticates with Anilist for the first time. Sets the refresh token
|
||||
* and the oauth object.
|
||||
*/
|
||||
fun setAuth(oauth: OAuth?) {
|
||||
refreshToken = oauth?.refresh_token
|
||||
this.oauth = oauth
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package eu.kanade.tachiyomi.data.track.anilist
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
data class ALManga(
|
||||
val id: Int,
|
||||
val title_romaji: String,
|
||||
val type: String,
|
||||
val total_chapters: Int) {
|
||||
|
||||
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
||||
remote_id = this@ALManga.id
|
||||
title = title_romaji
|
||||
total_chapters = this@ALManga.total_chapters
|
||||
}
|
||||
}
|
||||
|
||||
data class ALUserManga(
|
||||
val id: Int,
|
||||
val list_status: String,
|
||||
val score_raw: Int,
|
||||
val chapters_read: Int,
|
||||
val manga: ALManga) {
|
||||
|
||||
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
||||
remote_id = manga.id
|
||||
status = toTrackStatus()
|
||||
score = score_raw.toFloat()
|
||||
last_chapter_read = chapters_read
|
||||
}
|
||||
|
||||
fun toTrackStatus() = when (list_status) {
|
||||
"reading" -> Anilist.READING
|
||||
"completed" -> Anilist.COMPLETED
|
||||
"on-hold" -> Anilist.ON_HOLD
|
||||
"dropped" -> Anilist.DROPPED
|
||||
"plan to read" -> Anilist.PLAN_TO_READ
|
||||
else -> throw NotImplementedError("Unknown status")
|
||||
}
|
||||
}
|
||||
|
||||
data class ALUserLists(val lists: Map<String, List<ALUserManga>>) {
|
||||
|
||||
fun flatten() = lists.values.flatten()
|
||||
}
|
||||
|
||||
fun Track.toAnilistStatus() = when (status) {
|
||||
Anilist.READING -> "reading"
|
||||
Anilist.COMPLETED -> "completed"
|
||||
Anilist.ON_HOLD -> "on-hold"
|
||||
Anilist.DROPPED -> "dropped"
|
||||
Anilist.PLAN_TO_READ -> "plan to read"
|
||||
else -> throw NotImplementedError("Unknown status")
|
||||
}
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().getOrDefault()) {
|
||||
// 10 point
|
||||
0 -> (score.toInt() / 10).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")
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
||||
|
||||
data class OAuth(
|
||||
val access_token: String,
|
||||
val token_type: String,
|
||||
val expires: Long,
|
||||
val expires_in: Long,
|
||||
val refresh_token: String?) {
|
||||
|
||||
fun isExpired() = System.currentTimeMillis() > expires
|
||||
package eu.kanade.tachiyomi.data.track.anilist
|
||||
|
||||
data class OAuth(
|
||||
val access_token: String,
|
||||
val token_type: String,
|
||||
val expires: Long,
|
||||
val expires_in: Long,
|
||||
val refresh_token: String?) {
|
||||
|
||||
fun isExpired() = System.currentTimeMillis() > expires
|
||||
}
|
136
app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt
Normal file
136
app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt
Normal file
@ -0,0 +1,136 @@
|
||||
package eu.kanade.tachiyomi.data.track.kitsu
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
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 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(client, interceptor) }
|
||||
|
||||
override fun getLogo(): Int {
|
||||
return R.drawable.kitsu
|
||||
}
|
||||
|
||||
override fun getLogoColor(): Int {
|
||||
return Color.rgb(51, 37, 50)
|
||||
}
|
||||
|
||||
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 -> ""
|
||||
}
|
||||
}
|
||||
|
||||
override fun getScoreList(): List<String> {
|
||||
return IntRange(0, 10).map { (it.toFloat() / 2).toString() }
|
||||
}
|
||||
|
||||
override fun displayScore(track: Track): String {
|
||||
return track.toKitsuScore()
|
||||
}
|
||||
|
||||
override fun add(track: Track): Observable<Track> {
|
||||
return api.addLibManga(track, getUserId())
|
||||
}
|
||||
|
||||
override fun update(track: Track): Observable<Track> {
|
||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||
track.status = COMPLETED
|
||||
}
|
||||
|
||||
return api.updateLibManga(track)
|
||||
}
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
return api.findLibManga(track, getUserId())
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun search(query: String): Observable<List<Track>> {
|
||||
return api.search(query)
|
||||
}
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
return api.getLibManga(track)
|
||||
.map { remoteTrack ->
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
override fun login(username: String, password: String): Completable {
|
||||
return api.login(username, password)
|
||||
.doOnNext { interceptor.newAuth(it) }
|
||||
.flatMap { api.getCurrentUser() }
|
||||
.doOnNext { userId -> saveCredentials(username, userId) }
|
||||
.doOnError { logout() }
|
||||
.toCompletable()
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
super.logout()
|
||||
interceptor.newAuth(null)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,200 @@
|
||||
package eu.kanade.tachiyomi.data.track.kitsu
|
||||
|
||||
import com.github.salomonbrys.kotson.*
|
||||
import com.google.gson.JsonObject
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
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
|
||||
|
||||
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
|
||||
|
||||
private val rest = Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.client(client.newBuilder().addInterceptor(interceptor).build())
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||
.build()
|
||||
.create(KitsuApi.Rest::class.java)
|
||||
|
||||
fun addLibManga(track: Track, userId: String): Observable<Track> {
|
||||
return Observable.defer {
|
||||
// @formatter:off
|
||||
val data = jsonObject(
|
||||
"type" to "libraryEntries",
|
||||
"attributes" to jsonObject(
|
||||
"status" to track.toKitsuStatus(),
|
||||
"progress" to track.last_chapter_read
|
||||
),
|
||||
"relationships" to jsonObject(
|
||||
"user" to jsonObject(
|
||||
"data" to jsonObject(
|
||||
"id" to userId,
|
||||
"type" to "users"
|
||||
)
|
||||
),
|
||||
"media" to jsonObject(
|
||||
"data" to jsonObject(
|
||||
"id" to track.remote_id,
|
||||
"type" to "manga"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
// @formatter:on
|
||||
|
||||
rest.addLibManga(jsonObject("data" to data))
|
||||
.map { json ->
|
||||
track.remote_id = json["data"]["id"].int
|
||||
track
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track): Observable<Track> {
|
||||
return Observable.defer {
|
||||
// @formatter:off
|
||||
val data = jsonObject(
|
||||
"type" to "libraryEntries",
|
||||
"id" to track.remote_id,
|
||||
"attributes" to jsonObject(
|
||||
"status" to track.toKitsuStatus(),
|
||||
"progress" to track.last_chapter_read,
|
||||
"rating" to track.toKitsuScore()
|
||||
)
|
||||
)
|
||||
// @formatter:on
|
||||
|
||||
rest.updateLibManga(track.remote_id, jsonObject("data" to data))
|
||||
.map { track }
|
||||
}
|
||||
}
|
||||
|
||||
fun search(query: String): Observable<List<Track>> {
|
||||
return rest.search(query)
|
||||
.map { json ->
|
||||
val data = json["data"].array
|
||||
data.map { KitsuManga(it.obj) }
|
||||
.filter { it.type != "novel" }
|
||||
.map { it.toTrack() }
|
||||
}
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track, userId: String): Observable<Track?> {
|
||||
return rest.findLibManga(track.remote_id, userId)
|
||||
.map { json ->
|
||||
val data = json["data"].array
|
||||
if (data.size() > 0) {
|
||||
KitsuLibManga(data[0].obj, json["included"].array[0].obj).toTrack()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibManga(track: Track): Observable<Track> {
|
||||
return rest.getLibManga(track.remote_id)
|
||||
.map { json ->
|
||||
val data = json["data"].array
|
||||
if (data.size() > 0) {
|
||||
val include = json["included"].array[0].obj
|
||||
KitsuLibManga(data[0].obj, include).toTrack()
|
||||
} else {
|
||||
throw Exception("Could not find manga")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun login(username: String, password: String): Observable<OAuth> {
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(loginUrl)
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||
.build()
|
||||
.create(KitsuApi.LoginRest::class.java)
|
||||
.requestAccessToken(username, password)
|
||||
}
|
||||
|
||||
fun getCurrentUser(): Observable<String> {
|
||||
return rest.getCurrentUser().map { it["data"].array[0]["id"].string }
|
||||
}
|
||||
|
||||
private interface Rest {
|
||||
|
||||
@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>
|
||||
|
||||
@GET("manga")
|
||||
fun search(
|
||||
@Query("filter[text]", encoded = true) query: String
|
||||
): Observable<JsonObject>
|
||||
|
||||
@GET("library-entries")
|
||||
fun findLibManga(
|
||||
@Query("filter[media_id]", encoded = true) remoteId: Int,
|
||||
@Query("filter[user_id]", encoded = true) userId: String,
|
||||
@Query("page[limit]", encoded = true) limit: Int = 10000,
|
||||
@Query("include") includes: String = "media"
|
||||
): Observable<JsonObject>
|
||||
|
||||
@GET("library-entries")
|
||||
fun getLibManga(
|
||||
@Query("filter[id]", encoded = true) remoteId: Int,
|
||||
@Query("include") includes: String = "media"
|
||||
): Observable<JsonObject>
|
||||
|
||||
@GET("users")
|
||||
fun getCurrentUser(
|
||||
@Query("filter[self]", encoded = true) self: Boolean = true
|
||||
): Observable<JsonObject>
|
||||
|
||||
}
|
||||
|
||||
private interface LoginRest {
|
||||
|
||||
@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>
|
||||
|
||||
}
|
||||
|
||||
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 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())
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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,58 @@
|
||||
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"].obj.get("chapterCount").nullInt
|
||||
val type = obj["attributes"].obj.get("mangaType").nullString
|
||||
|
||||
@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"].obj.get("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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun Track.toKitsuStatus() = when (status) {
|
||||
Kitsu.READING -> "current"
|
||||
Kitsu.COMPLETED -> "completed"
|
||||
Kitsu.ON_HOLD -> "on_hold"
|
||||
Kitsu.DROPPED -> "dropped"
|
||||
Kitsu.PLAN_TO_READ -> "planned"
|
||||
else -> throw Exception("Unknown status")
|
||||
}
|
||||
|
||||
fun Track.toKitsuScore(): String {
|
||||
return if (score > 0) (score / 2).toString() else ""
|
||||
}
|
@ -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)
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
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
|
||||
|
||||
class Myanimelist(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 = 6
|
||||
|
||||
const val DEFAULT_STATUS = READING
|
||||
const val DEFAULT_SCORE = 0
|
||||
}
|
||||
|
||||
private val api by lazy { MyanimelistApi(client, getUsername(), getPassword()) }
|
||||
|
||||
override val name: String
|
||||
get() = "MyAnimeList"
|
||||
|
||||
override fun getLogo() = R.drawable.mal
|
||||
|
||||
override fun getLogoColor() = Color.rgb(46, 81, 162)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override fun getScoreList(): List<String> {
|
||||
return IntRange(0, 10).map(Int::toString)
|
||||
}
|
||||
|
||||
override fun displayScore(track: Track): String {
|
||||
return track.score.toInt().toString()
|
||||
}
|
||||
|
||||
override fun add(track: Track): Observable<Track> {
|
||||
return api.addLibManga(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.updateLibManga(track)
|
||||
}
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
return api.findLibManga(track, getUsername())
|
||||
.flatMap { remoteTrack ->
|
||||
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 search(query: String): Observable<List<Track>> {
|
||||
return api.search(query, getUsername())
|
||||
}
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
return api.getLibManga(track, getUsername())
|
||||
.map { remoteTrack ->
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
override fun login(username: String, password: String): Completable {
|
||||
return api.login(username, password)
|
||||
.doOnNext { saveCredentials(username, password) }
|
||||
.doOnError { logout() }
|
||||
.toCompletable()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,187 @@
|
||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Xml
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.network.GET
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import eu.kanade.tachiyomi.data.network.asObservable
|
||||
import eu.kanade.tachiyomi.data.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.util.selectInt
|
||||
import eu.kanade.tachiyomi.util.selectText
|
||||
import okhttp3.*
|
||||
import org.jsoup.Jsoup
|
||||
import org.xmlpull.v1.XmlSerializer
|
||||
import rx.Observable
|
||||
import java.io.StringWriter
|
||||
|
||||
class MyanimelistApi(private val client: OkHttpClient, username: String, password: String) {
|
||||
|
||||
private var headers = createHeaders(username, password)
|
||||
|
||||
fun addLibManga(track: Track): Observable<Track> {
|
||||
return Observable.defer {
|
||||
client.newCall(POST(getAddUrl(track), headers, getMangaPostPayload(track)))
|
||||
.asObservableSuccess()
|
||||
.map { track }
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track): Observable<Track> {
|
||||
return Observable.defer {
|
||||
client.newCall(POST(getUpdateUrl(track), headers, getMangaPostPayload(track)))
|
||||
.asObservableSuccess()
|
||||
.map { track }
|
||||
}
|
||||
}
|
||||
|
||||
fun search(query: String, username: String): Observable<List<Track>> {
|
||||
return if (query.startsWith(PREFIX_MY)) {
|
||||
val realQuery = query.substring(PREFIX_MY.length).toLowerCase().trim()
|
||||
getList(username)
|
||||
.flatMap { Observable.from(it) }
|
||||
.filter { realQuery in it.title.toLowerCase() }
|
||||
.toList()
|
||||
} else {
|
||||
client.newCall(GET(getSearchUrl(query), headers))
|
||||
.asObservable()
|
||||
.map { Jsoup.parse(it.body().string()) }
|
||||
.flatMap { Observable.from(it.select("entry")) }
|
||||
.filter { it.select("type").text() != "Novel" }
|
||||
.map {
|
||||
Track.create(TrackManager.MYANIMELIST).apply {
|
||||
title = it.selectText("title")!!
|
||||
remote_id = it.selectInt("id")
|
||||
total_chapters = it.selectInt("chapters")
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
|
||||
fun getList(username: String): Observable<List<Track>> {
|
||||
return client
|
||||
.newCall(GET(getListUrl(username), headers))
|
||||
.asObservable()
|
||||
.map { Jsoup.parse(it.body().string()) }
|
||||
.flatMap { Observable.from(it.select("manga")) }
|
||||
.map {
|
||||
Track.create(TrackManager.MYANIMELIST).apply {
|
||||
title = it.selectText("series_title")!!
|
||||
remote_id = it.selectInt("series_mangadb_id")
|
||||
last_chapter_read = it.selectInt("my_read_chapters")
|
||||
status = it.selectInt("my_status")
|
||||
score = it.selectInt("my_score").toFloat()
|
||||
total_chapters = it.selectInt("series_chapters")
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track, username: String): Observable<Track?> {
|
||||
return getList(username)
|
||||
.map { list -> list.find { it.remote_id == track.remote_id } }
|
||||
}
|
||||
|
||||
fun getLibManga(track: Track, username: String): Observable<Track> {
|
||||
return findLibManga(track, username)
|
||||
.map { it ?: throw Exception("Could not find manga") }
|
||||
}
|
||||
|
||||
fun login(username: String, password: String): Observable<Response> {
|
||||
headers = createHeaders(username, password)
|
||||
return client.newCall(GET(getLoginUrl(), headers))
|
||||
.asObservable()
|
||||
.doOnNext { response ->
|
||||
response.close()
|
||||
if (response.code() != 200) throw Exception("Login error")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMangaPostPayload(track: Track): RequestBody {
|
||||
val data = xml {
|
||||
element(ENTRY_TAG) {
|
||||
if (track.last_chapter_read != 0) {
|
||||
text(CHAPTER_TAG, track.last_chapter_read.toString())
|
||||
}
|
||||
text(STATUS_TAG, track.status.toString())
|
||||
text(SCORE_TAG, track.score.toString())
|
||||
}
|
||||
}
|
||||
|
||||
return FormBody.Builder()
|
||||
.add("data", data)
|
||||
.build()
|
||||
}
|
||||
|
||||
private inline fun xml(block: XmlSerializer.() -> Unit): String {
|
||||
val x = Xml.newSerializer()
|
||||
val writer = StringWriter()
|
||||
|
||||
with(x) {
|
||||
setOutput(writer)
|
||||
startDocument("UTF-8", false)
|
||||
block()
|
||||
endDocument()
|
||||
}
|
||||
|
||||
return writer.toString()
|
||||
}
|
||||
|
||||
private inline fun XmlSerializer.element(tag: String, block: XmlSerializer.() -> Unit) {
|
||||
startTag("", tag)
|
||||
block()
|
||||
endTag("", tag)
|
||||
}
|
||||
|
||||
private fun XmlSerializer.text(tag: String, body: String) {
|
||||
startTag("", tag)
|
||||
text(body)
|
||||
endTag("", tag)
|
||||
}
|
||||
|
||||
fun getLoginUrl() = Uri.parse(baseUrl).buildUpon()
|
||||
.appendEncodedPath("api/account/verify_credentials.xml")
|
||||
.toString()
|
||||
|
||||
fun getSearchUrl(query: String) = Uri.parse(baseUrl).buildUpon()
|
||||
.appendEncodedPath("api/manga/search.xml")
|
||||
.appendQueryParameter("q", query)
|
||||
.toString()
|
||||
|
||||
fun getListUrl(username: String) = Uri.parse(baseUrl).buildUpon()
|
||||
.appendPath("malappinfo.php")
|
||||
.appendQueryParameter("u", username)
|
||||
.appendQueryParameter("status", "all")
|
||||
.appendQueryParameter("type", "manga")
|
||||
.toString()
|
||||
|
||||
fun getUpdateUrl(track: Track) = Uri.parse(baseUrl).buildUpon()
|
||||
.appendEncodedPath("api/mangalist/update")
|
||||
.appendPath("${track.remote_id}.xml")
|
||||
.toString()
|
||||
|
||||
fun getAddUrl(track: Track) = Uri.parse(baseUrl).buildUpon()
|
||||
.appendEncodedPath("api/mangalist/add")
|
||||
.appendPath("${track.remote_id}.xml")
|
||||
.toString()
|
||||
|
||||
fun createHeaders(username: String, password: String): Headers {
|
||||
return Headers.Builder()
|
||||
.add("Authorization", Credentials.basic(username, password))
|
||||
.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
|
||||
.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val baseUrl = "https://myanimelist.net"
|
||||
|
||||
private val ENTRY_TAG = "entry"
|
||||
private val CHAPTER_TAG = "chapter"
|
||||
private val SCORE_TAG = "score"
|
||||
private val STATUS_TAG = "status"
|
||||
|
||||
const val PREFIX_MY = "my:"
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.updater
|
||||
import android.app.IntentService
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
|
||||
import eu.kanade.tachiyomi.R
|
||||
@ -36,21 +35,6 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
|
||||
}
|
||||
context.startService(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt user with apk install intent
|
||||
* @param context context
|
||||
* @param file file of apk that is installed
|
||||
*/
|
||||
fun installAPK(context: Context, file: File) {
|
||||
// Prompt install interface
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive")
|
||||
// Without this flag android returned a intent error!
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,7 +90,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
|
||||
throw Exception("Unsuccessful response")
|
||||
}
|
||||
|
||||
val installIntent = UpdateNotificationReceiver.installApkIntent(ctx, apkFile.absolutePath)
|
||||
val installIntent = UpdateNotificationReceiver.installApkIntent(ctx, apkFile)
|
||||
|
||||
// Prompt the user to install the new update.
|
||||
NotificationCompat.Builder(this).update {
|
||||
|
@ -4,6 +4,10 @@ import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.support.v4.content.FileProvider
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
|
||||
import eu.kanade.tachiyomi.util.notificationManager
|
||||
import java.io.File
|
||||
@ -12,34 +16,14 @@ class UpdateNotificationReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
ACTION_INSTALL_APK -> {
|
||||
UpdateDownloaderService.installAPK(context,
|
||||
File(intent.getStringExtra(EXTRA_FILE_LOCATION)))
|
||||
cancelNotification(context)
|
||||
}
|
||||
ACTION_DOWNLOAD_UPDATE -> UpdateDownloaderService.downloadUpdate(context,
|
||||
intent.getStringExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL))
|
||||
ACTION_CANCEL_NOTIFICATION -> cancelNotification(context)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelNotification(context: Context) {
|
||||
context.notificationManager.cancel(NOTIFICATION_UPDATER_ID)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Install apk action
|
||||
const val ACTION_INSTALL_APK = "eu.kanade.INSTALL_APK"
|
||||
|
||||
// Download apk action
|
||||
const val ACTION_DOWNLOAD_UPDATE = "eu.kanade.RETRY_DOWNLOAD"
|
||||
|
||||
// Cancel notification action
|
||||
const val ACTION_CANCEL_NOTIFICATION = "eu.kanade.CANCEL_NOTIFICATION"
|
||||
|
||||
// Absolute path of apk file
|
||||
const val EXTRA_FILE_LOCATION = "file_location"
|
||||
|
||||
fun cancelNotificationIntent(context: Context): PendingIntent {
|
||||
val intent = Intent(context, UpdateNotificationReceiver::class.java).apply {
|
||||
action = ACTION_CANCEL_NOTIFICATION
|
||||
@ -47,20 +31,39 @@ class UpdateNotificationReceiver : BroadcastReceiver() {
|
||||
return PendingIntent.getBroadcast(context, 0, intent, 0)
|
||||
}
|
||||
|
||||
fun installApkIntent(context: Context, path: String): PendingIntent {
|
||||
val intent = Intent(context, UpdateNotificationReceiver::class.java).apply {
|
||||
action = ACTION_INSTALL_APK
|
||||
putExtra(EXTRA_FILE_LOCATION, path)
|
||||
/**
|
||||
* Prompt user with apk install intent
|
||||
*
|
||||
* @param context context
|
||||
* @param file file of apk that is installed
|
||||
*/
|
||||
fun installApkIntent(context: Context, file: File): PendingIntent {
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
|
||||
else Uri.fromFile(file)
|
||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, 0, intent, 0)
|
||||
cancelNotification(context)
|
||||
return PendingIntent.getActivity(context, 0, intent, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a new update and let the user install the new version from a notification.
|
||||
*
|
||||
* @param context the application context.
|
||||
* @param url the url to the new update.
|
||||
*/
|
||||
fun downloadApkIntent(context: Context, url: String): PendingIntent {
|
||||
val intent = Intent(context, UpdateNotificationReceiver::class.java).apply {
|
||||
action = ACTION_DOWNLOAD_UPDATE
|
||||
val intent = Intent(context, UpdateDownloaderService::class.java).apply {
|
||||
putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url)
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, 0, intent, 0)
|
||||
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
fun cancelNotification(context: Context) {
|
||||
context.notificationManager.cancel(NOTIFICATION_UPDATER_ID)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,11 +15,17 @@ import uy.kohesive.injekt.api.get
|
||||
|
||||
interface ActivityMixin {
|
||||
|
||||
var resumed: Boolean
|
||||
|
||||
fun setupToolbar(toolbar: Toolbar, backNavigation: Boolean = true) {
|
||||
setSupportActionBar(toolbar)
|
||||
getSupportActionBar()?.setDisplayHomeAsUpEnabled(true)
|
||||
if (backNavigation) {
|
||||
toolbar.setNavigationOnClickListener { onBackPressed() }
|
||||
toolbar.setNavigationOnClickListener {
|
||||
if (resumed) {
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,15 +5,14 @@ import eu.kanade.tachiyomi.util.LocaleHelper
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity(), ActivityMixin {
|
||||
|
||||
override var resumed = false
|
||||
|
||||
init {
|
||||
LocaleHelper.updateCfg(this)
|
||||
LocaleHelper.updateConfiguration(this)
|
||||
}
|
||||
|
||||
override fun getActivity() = this
|
||||
|
||||
var resumed = false
|
||||
private set
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
resumed = true
|
||||
|
@ -8,8 +8,10 @@ import nucleus.view.NucleusAppCompatActivity
|
||||
|
||||
abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P>(), ActivityMixin {
|
||||
|
||||
override var resumed = false
|
||||
|
||||
init {
|
||||
LocaleHelper.updateCfg(this)
|
||||
LocaleHelper.updateConfiguration(this)
|
||||
}
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
@ -25,9 +27,6 @@ abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P
|
||||
|
||||
override fun getActivity() = this
|
||||
|
||||
var resumed = false
|
||||
private set
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
resumed = true
|
||||
|
@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.source.EN
|
||||
import eu.kanade.tachiyomi.data.source.Source
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||
@ -333,13 +332,13 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
|
||||
// Ensure at least one language
|
||||
if (languages.isEmpty()) {
|
||||
languages.add(EN.code)
|
||||
languages.add("en")
|
||||
}
|
||||
|
||||
return sourceManager.getOnlineSources()
|
||||
.filter { it.lang.code in languages }
|
||||
.filter { it.lang in languages }
|
||||
.filterNot { it.id.toString() in hiddenCatalogues }
|
||||
.sortedBy { "(${it.lang.code}) ${it.name}" }
|
||||
.sortedBy { "(${it.lang}) ${it.name}" }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -293,9 +293,12 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
||||
*
|
||||
* @param mangas the list of manga.
|
||||
*/
|
||||
fun getCommonCategories(mangas: List<Manga>): Collection<Category> = mangas.toSet()
|
||||
.map { db.getCategoriesForManga(it).executeAsBlocking() }
|
||||
.reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2) }
|
||||
fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
|
||||
if (mangas.isEmpty()) return emptyList()
|
||||
return mangas.toSet()
|
||||
.map { db.getCategoriesForManga(it).executeAsBlocking() }
|
||||
.reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the selected manga from the library.
|
||||
|
@ -93,7 +93,9 @@ class MainActivity : BaseActivity() {
|
||||
|
||||
override fun onBackPressed() {
|
||||
val fragment = supportFragmentManager.findFragmentById(R.id.frame_container)
|
||||
if (fragment != null && fragment.tag.toInt() != startScreenId) {
|
||||
if (drawer.isDrawerOpen(GravityCompat.START) || drawer.isDrawerOpen(GravityCompat.END)) {
|
||||
drawer.closeDrawers()
|
||||
} else if (fragment != null && fragment.tag.toInt() != startScreenId) {
|
||||
if (resumed) {
|
||||
setSelectedDrawerItem(startScreenId)
|
||||
}
|
||||
@ -140,4 +142,4 @@ class MainActivity : BaseActivity() {
|
||||
companion object {
|
||||
private const val REQUEST_OPEN_SETTINGS = 200
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,15 +3,18 @@ package eu.kanade.tachiyomi.ui.manga
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.graphics.drawable.VectorDrawableCompat
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v4.app.FragmentPagerAdapter
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment
|
||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoFragment
|
||||
import eu.kanade.tachiyomi.ui.manga.myanimelist.MyAnimeListFragment
|
||||
import eu.kanade.tachiyomi.ui.manga.track.TrackFragment
|
||||
import eu.kanade.tachiyomi.util.SharedData
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import kotlinx.android.synthetic.main.activity_manga.*
|
||||
@ -28,7 +31,7 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
|
||||
const val FROM_LAUNCHER_EXTRA = "from_launcher"
|
||||
const val INFO_FRAGMENT = 0
|
||||
const val CHAPTERS_FRAGMENT = 1
|
||||
const val MYANIMELIST_FRAGMENT = 2
|
||||
const val TRACK_FRAGMENT = 2
|
||||
|
||||
fun newIntent(context: Context, manga: Manga, fromCatalogue: Boolean = false): Intent {
|
||||
SharedData.put(MangaEvent(manga))
|
||||
@ -71,6 +74,7 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
|
||||
fromCatalogue = intent.getBooleanExtra(FROM_CATALOGUE_EXTRA, false)
|
||||
|
||||
adapter = MangaDetailAdapter(supportFragmentManager, this)
|
||||
view_pager.offscreenPageLimit = 3
|
||||
view_pager.adapter = adapter
|
||||
|
||||
tabs.setupWithViewPager(view_pager)
|
||||
@ -85,33 +89,50 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
|
||||
setToolbarTitle(manga.title)
|
||||
}
|
||||
|
||||
internal class MangaDetailAdapter(fm: FragmentManager, activity: MangaActivity) : FragmentPagerAdapter(fm) {
|
||||
fun setTrackingIcon(visible: Boolean) {
|
||||
val tab = tabs.getTabAt(TRACK_FRAGMENT) ?: return
|
||||
val drawable = if (visible)
|
||||
VectorDrawableCompat.create(resources, R.drawable.ic_done_white_18dp, null)
|
||||
else null
|
||||
|
||||
private var pageCount: Int = 0
|
||||
private val tabTitles = arrayOf(activity.getString(R.string.manga_detail_tab),
|
||||
activity.getString(R.string.manga_chapters_tab), "MAL")
|
||||
// I had no choice but to use reflection...
|
||||
val field = tab.javaClass.getDeclaredField("mView").apply { isAccessible = true }
|
||||
val view = field.get(tab) as LinearLayout
|
||||
val textView = view.getChildAt(1) as TextView
|
||||
textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null)
|
||||
textView.compoundDrawablePadding = 4
|
||||
}
|
||||
|
||||
private class MangaDetailAdapter(fm: FragmentManager, activity: MangaActivity)
|
||||
: FragmentPagerAdapter(fm) {
|
||||
|
||||
private var tabCount = 2
|
||||
|
||||
private val tabTitles = listOf(
|
||||
R.string.manga_detail_tab,
|
||||
R.string.manga_chapters_tab,
|
||||
R.string.manga_tracking_tab)
|
||||
.map { activity.getString(it) }
|
||||
|
||||
init {
|
||||
pageCount = 2
|
||||
if (!activity.fromCatalogue && activity.presenter.syncManager.myAnimeList.isLogged)
|
||||
pageCount++
|
||||
if (!activity.fromCatalogue && activity.presenter.trackManager.hasLoggedServices())
|
||||
tabCount++
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return pageCount
|
||||
return tabCount
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Fragment? {
|
||||
override fun getItem(position: Int): Fragment {
|
||||
when (position) {
|
||||
INFO_FRAGMENT -> return MangaInfoFragment.newInstance()
|
||||
CHAPTERS_FRAGMENT -> return ChaptersFragment.newInstance()
|
||||
MYANIMELIST_FRAGMENT -> return MyAnimeListFragment.newInstance()
|
||||
else -> return null
|
||||
TRACK_FRAGMENT -> return TrackFragment.newInstance()
|
||||
else -> throw Exception("Unknown position")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPageTitle(position: Int): CharSequence {
|
||||
// Generate title based on item position
|
||||
return tabTitles[position]
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.manga
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
|
||||
import eu.kanade.tachiyomi.util.SharedData
|
||||
@ -22,9 +22,9 @@ class MangaPresenter : BasePresenter<MangaActivity>() {
|
||||
val db: DatabaseHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Manga sync manager.
|
||||
* Tracking manager.
|
||||
*/
|
||||
val syncManager: MangaSyncManager by injectLazy()
|
||||
val trackManager: TrackManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Manga associated with this instance.
|
||||
|
@ -118,7 +118,7 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
// Initialize menu items.
|
||||
val menuFilterRead = menu.findItem(R.id.action_filter_read)
|
||||
val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return
|
||||
val menuFilterUnread = menu.findItem(R.id.action_filter_unread)
|
||||
val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded)
|
||||
val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked)
|
||||
|
@ -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,173 @@
|
||||
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
|
||||
val scores = item.service.getScoreList().toTypedArray()
|
||||
np.maxValue = scores.size - 1
|
||||
np.displayedValues = scores
|
||||
|
||||
// Set initial value
|
||||
val displayedScore = item.service.displayScore(item.track)
|
||||
if (displayedScore != "-") {
|
||||
val index = scores.indexOf(displayedScore)
|
||||
np.value = if (index != -1) index else 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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.displayScore(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, index: Int) {
|
||||
val track = item.track!!
|
||||
track.score = item.service.indexToScore(index)
|
||||
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() {
|
||||
val chapterToUpdate = presenter.getMangaSyncChapterToUpdate()
|
||||
val chapterToUpdate = presenter.getTrackChapterToUpdate()
|
||||
|
||||
if (chapterToUpdate > 0) {
|
||||
if (preferences.askUpdateMangaSync()) {
|
||||
if (preferences.askUpdateTrack()) {
|
||||
MaterialDialog.Builder(this)
|
||||
.content(getString(R.string.confirm_update_manga_sync, chapterToUpdate))
|
||||
.positiveText(android.R.string.yes)
|
||||
.negativeText(android.R.string.no)
|
||||
.onPositive { dialog, which -> presenter.updateMangaSyncLastChapterRead() }
|
||||
.onPositive { dialog, which -> presenter.updateTrackLastChapterRead() }
|
||||
.onAny { dialog1, which1 -> super.onBackPressed() }
|
||||
.show()
|
||||
} else {
|
||||
presenter.updateMangaSyncLastChapterRead()
|
||||
presenter.updateTrackLastChapterRead()
|
||||
super.onBackPressed()
|
||||
}
|
||||
} else {
|
||||
|
@ -10,14 +10,14 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackUpdateService
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.reader.notification.ImageNotifier
|
||||
import eu.kanade.tachiyomi.util.DiskUtil
|
||||
@ -54,9 +54,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
val downloadManager: DownloadManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Sync manager.
|
||||
* Tracking manager.
|
||||
*/
|
||||
val syncManager: MangaSyncManager by injectLazy()
|
||||
val trackManager: TrackManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Source manager.
|
||||
@ -124,7 +124,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
/**
|
||||
* List of manga services linked to the active manga, or null if auto syncing is not enabled.
|
||||
*/
|
||||
private var mangaSyncList: List<MangaSync>? = null
|
||||
private var trackList: List<Track>? = null
|
||||
|
||||
/**
|
||||
* Chapter loader whose job is to obtain the chapter list and initialize every page.
|
||||
@ -141,6 +141,11 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
*/
|
||||
private var adjacentChaptersSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Whether the active chapter has been loaded.
|
||||
*/
|
||||
private var chapterLoaded = false
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Id of the restartable that loads the active chapter.
|
||||
@ -165,9 +170,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
.subscribeLatestCache({ view, manga -> view.onMangaOpen(manga) })
|
||||
|
||||
// Retrieve the sync list if auto syncing is enabled.
|
||||
if (prefs.autoUpdateMangaSync()) {
|
||||
add(db.getMangasSync(manga).asRxSingle()
|
||||
.subscribe({ mangaSyncList = it }))
|
||||
if (prefs.autoUpdateTrack()) {
|
||||
add(db.getTracks(manga).asRxSingle()
|
||||
.subscribe({ trackList = it }))
|
||||
}
|
||||
|
||||
restartableLatestCache(LOAD_ACTIVE_CHAPTER,
|
||||
@ -211,6 +216,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
return loader.loadChapter(chapter)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { chapterLoaded = true }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -298,6 +304,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
nextChapter = null
|
||||
prevChapter = null
|
||||
|
||||
chapterLoaded = false
|
||||
start(LOAD_ACTIVE_CHAPTER)
|
||||
getAdjacentChapters(chapter)
|
||||
}
|
||||
@ -431,9 +438,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
/**
|
||||
* Returns the chapter to be marked as last read in sync services or 0 if no update required.
|
||||
*/
|
||||
fun getMangaSyncChapterToUpdate(): Int {
|
||||
val mangaSyncList = mangaSyncList
|
||||
if (chapter.pages == null || mangaSyncList == null || mangaSyncList.isEmpty())
|
||||
fun getTrackChapterToUpdate(): Int {
|
||||
val trackList = trackList
|
||||
if (chapter.pages == null || trackList == null || trackList.isEmpty())
|
||||
return 0
|
||||
|
||||
val prevChapter = prevChapter
|
||||
@ -446,24 +453,24 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
else
|
||||
0
|
||||
|
||||
mangaSyncList.forEach { sync ->
|
||||
trackList.forEach { sync ->
|
||||
if (lastChapterRead > sync.last_chapter_read) {
|
||||
sync.last_chapter_read = lastChapterRead
|
||||
sync.update = true
|
||||
}
|
||||
}
|
||||
|
||||
return if (mangaSyncList.any { it.update }) lastChapterRead else 0
|
||||
return if (trackList.any { it.update }) lastChapterRead else 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the service that updates the last chapter read in sync services
|
||||
*/
|
||||
fun updateMangaSyncLastChapterRead() {
|
||||
mangaSyncList?.forEach { sync ->
|
||||
val service = syncManager.getService(sync.sync_id)
|
||||
fun updateTrackLastChapterRead() {
|
||||
trackList?.forEach { sync ->
|
||||
val service = trackManager.getService(sync.sync_id)
|
||||
if (service != null && service.isLogged && sync.update) {
|
||||
UpdateMangaSyncService.start(context, sync)
|
||||
TrackUpdateService.start(context, sync)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -474,6 +481,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
* @return true if the next chapter is being loaded, false if there is no next chapter.
|
||||
*/
|
||||
fun loadNextChapter(): Boolean {
|
||||
// Avoid skipping chapters.
|
||||
if (!chapterLoaded) return true
|
||||
|
||||
nextChapter?.let {
|
||||
onChapterLeft()
|
||||
loadChapter(it, 0)
|
||||
@ -488,6 +498,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
* @return true if the previous chapter is being loaded, false if there is no previous chapter.
|
||||
*/
|
||||
fun loadPreviousChapter(): Boolean {
|
||||
// Avoid skipping chapters.
|
||||
if (!chapterLoaded) return true
|
||||
|
||||
prevChapter?.let {
|
||||
onChapterLeft()
|
||||
loadChapter(it, if (it.read) -1 else 0)
|
||||
|
@ -45,14 +45,14 @@ class ImageNotificationReceiver : BroadcastReceiver() {
|
||||
* Called to start share intent to share image
|
||||
*
|
||||
* @param context context of application
|
||||
* @param path path of file
|
||||
* @param file file that contains image
|
||||
*/
|
||||
internal fun shareImageIntent(context: Context, path: String): PendingIntent {
|
||||
internal fun shareImageIntent(context: Context, file: File): PendingIntent {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", File(path))
|
||||
val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
type = "image/*"
|
||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
}
|
||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
@ -61,15 +61,15 @@ class ImageNotificationReceiver : BroadcastReceiver() {
|
||||
* Called to show image in gallery application
|
||||
*
|
||||
* @param context context of application
|
||||
* @param path path of file
|
||||
* @param file file that contains image
|
||||
*/
|
||||
internal fun showImageIntent(context: Context, path: String): PendingIntent {
|
||||
internal fun showImageIntent(context: Context, file: File): PendingIntent {
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", File(path))
|
||||
val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
|
||||
setDataAndType(uri, "image/*")
|
||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
}
|
||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
return PendingIntent.getActivity(context, 0, intent, 0)
|
||||
}
|
||||
|
||||
internal fun deleteImageIntent(context: Context, path: String, notificationId: Int): PendingIntent {
|
||||
|
@ -58,11 +58,11 @@ class ImageNotifier(private val context: Context) {
|
||||
if (!mActions.isEmpty())
|
||||
mActions.clear()
|
||||
|
||||
setContentIntent(ImageNotificationReceiver.showImageIntent(context, file.absolutePath))
|
||||
setContentIntent(ImageNotificationReceiver.showImageIntent(context, file))
|
||||
// Share action
|
||||
addAction(R.drawable.ic_share_grey_24dp,
|
||||
context.getString(R.string.action_share),
|
||||
ImageNotificationReceiver.shareImageIntent(context, file.absolutePath))
|
||||
context.getString(R.string.action_share),
|
||||
ImageNotificationReceiver.shareImageIntent(context, file))
|
||||
// Delete action
|
||||
addAction(R.drawable.ic_delete_grey_24dp,
|
||||
context.getString(R.string.action_delete),
|
||||
|
@ -5,15 +5,18 @@ import android.support.v4.app.DialogFragment
|
||||
import android.support.v7.view.ActionMode
|
||||
import android.support.v7.widget.DividerItemDecoration
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.*
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import eu.kanade.tachiyomi.widget.DeletingChaptersDialog
|
||||
import kotlinx.android.synthetic.main.fragment_recent_chapters.*
|
||||
import nucleus.factory.RequiresPresenter
|
||||
@ -73,6 +76,25 @@ class RecentChaptersFragment
|
||||
adapter = RecentChaptersAdapter(this)
|
||||
recycler.adapter = adapter
|
||||
|
||||
recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) {
|
||||
// Disable swipe refresh when view is not at the top
|
||||
val firstPos = (recycler.layoutManager as LinearLayoutManager)
|
||||
.findFirstCompletelyVisibleItemPosition()
|
||||
swipe_refresh.isEnabled = firstPos == 0
|
||||
}
|
||||
})
|
||||
|
||||
swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
|
||||
swipe_refresh.setOnRefreshListener {
|
||||
if (!LibraryUpdateService.isRunning(activity)) {
|
||||
LibraryUpdateService.start(activity)
|
||||
context.toast(R.string.action_update_library)
|
||||
}
|
||||
// It can be a very long operation, so we disable swipe refresh and show a toast.
|
||||
swipe_refresh.isRefreshing = false
|
||||
}
|
||||
|
||||
// Update toolbar text
|
||||
setToolbarTitle(R.string.label_recent_updates)
|
||||
}
|
||||
|
@ -7,14 +7,14 @@ import android.view.Gravity.CENTER
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ProgressBar
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class AnilistLoginActivity : AppCompatActivity() {
|
||||
|
||||
private val syncManager: MangaSyncManager by injectLazy()
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
@ -24,7 +24,7 @@ class AnilistLoginActivity : AppCompatActivity() {
|
||||
|
||||
val code = intent.data?.getQueryParameter("code")
|
||||
if (code != null) {
|
||||
syncManager.aniList.login(code)
|
||||
trackManager.aniList.login(code)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({
|
||||
@ -33,7 +33,7 @@ class AnilistLoginActivity : AppCompatActivity() {
|
||||
returnToSettings()
|
||||
})
|
||||
} else {
|
||||
syncManager.aniList.logout()
|
||||
trackManager.aniList.logout()
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ class SettingsActivity : BaseActivity(),
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
setAppTheme()
|
||||
super.onCreate(savedState)
|
||||
setTitle(R.string.label_settings)
|
||||
setContentView(R.layout.activity_preferences)
|
||||
|
||||
replaceFragmentStrategy = ReplaceFragment(this,
|
||||
@ -63,7 +64,7 @@ class SettingsActivity : BaseActivity(),
|
||||
"general_screen" -> SettingsGeneralFragment.newInstance(key)
|
||||
"downloads_screen" -> SettingsDownloadsFragment.newInstance(key)
|
||||
"sources_screen" -> SettingsSourcesFragment.newInstance(key)
|
||||
"sync_screen" -> SettingsSyncFragment.newInstance(key)
|
||||
"tracking_screen" -> SettingsTrackingFragment.newInstance(key)
|
||||
"advanced_screen" -> SettingsAdvancedFragment.newInstance(key)
|
||||
"about_screen" -> SettingsAboutFragment.newInstance(key)
|
||||
else -> SettingsFragment.newInstance(key)
|
||||
|
@ -28,7 +28,7 @@ open class SettingsFragment : XpPreferenceFragment() {
|
||||
addPreferencesFromResource(R.xml.pref_reader)
|
||||
addPreferencesFromResource(R.xml.pref_downloads)
|
||||
addPreferencesFromResource(R.xml.pref_sources)
|
||||
addPreferencesFromResource(R.xml.pref_sync)
|
||||
addPreferencesFromResource(R.xml.pref_tracking)
|
||||
addPreferencesFromResource(R.xml.pref_advanced)
|
||||
addPreferencesFromResource(R.xml.pref_about)
|
||||
|
||||
|
@ -14,11 +14,11 @@ import eu.kanade.tachiyomi.util.plusAssign
|
||||
import eu.kanade.tachiyomi.widget.preference.IntListPreference
|
||||
import eu.kanade.tachiyomi.widget.preference.LibraryColumnsDialog
|
||||
import eu.kanade.tachiyomi.widget.preference.SimpleDialogPreference
|
||||
import net.xpece.android.support.preference.ListPreference
|
||||
import net.xpece.android.support.preference.MultiSelectListPreference
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.*
|
||||
|
||||
class SettingsGeneralFragment : SettingsFragment(),
|
||||
PreferenceFragmentCompat.OnPreferenceDisplayDialogCallback {
|
||||
@ -46,7 +46,7 @@ class SettingsGeneralFragment : SettingsFragment(),
|
||||
|
||||
val categoryUpdate: MultiSelectListPreference by bindPref(R.string.pref_library_update_categories_key)
|
||||
|
||||
val langPreference: IntListPreference by bindPref(R.string.pref_language_key)
|
||||
val langPreference: ListPreference by bindPref(R.string.pref_language_key)
|
||||
|
||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||
super.onViewCreated(view, savedState)
|
||||
@ -106,10 +106,17 @@ class SettingsGeneralFragment : SettingsFragment(),
|
||||
true
|
||||
}
|
||||
|
||||
val langValues = langPreference.entryValues.map { value ->
|
||||
val locale = LocaleHelper.getLocaleFromString(value.toString())
|
||||
locale?.getDisplayName(locale)?.capitalize() ?: context.getString(R.string.system_default)
|
||||
}
|
||||
|
||||
langPreference.entries = langValues.toTypedArray()
|
||||
langPreference.setOnPreferenceChangeListener { preference, newValue ->
|
||||
(activity as SettingsActivity).parentFlags = SettingsActivity.FLAG_LANG_CHANGED
|
||||
LocaleHelper.setLocale(Locale(LocaleHelper.intToLangCode(newValue.toString().toInt())))
|
||||
LocaleHelper.updateCfg(activity.application, activity.baseContext.resources.configuration)
|
||||
LocaleHelper.changeLocale(newValue.toString())
|
||||
val app = activity.application
|
||||
LocaleHelper.updateConfiguration(app, app.resources.configuration)
|
||||
activity.recreate()
|
||||
true
|
||||
}
|
||||
|
@ -8,13 +8,14 @@ import android.view.View
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.data.source.getLanguages
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.widget.preference.LoginCheckBoxPreference
|
||||
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
|
||||
import eu.kanade.tachiyomi.widget.preference.SwitchPreferenceCategory
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.*
|
||||
|
||||
class SettingsSourcesFragment : SettingsFragment() {
|
||||
|
||||
@ -45,33 +46,35 @@ class SettingsSourcesFragment : SettingsFragment() {
|
||||
// Get the list of active language codes.
|
||||
val activeLangsCodes = preferences.enabledLanguages().getOrDefault()
|
||||
|
||||
// Get the list of languages ordered by name.
|
||||
val langs = getLanguages().sortedBy { it.lang }
|
||||
// Get a map of sources grouped by language.
|
||||
val sourcesByLang = onlineSources.groupByTo(TreeMap(), { it.lang })
|
||||
|
||||
// Order first by active languages, then inactive ones
|
||||
val orderedLangs = langs.filter { it.code in activeLangsCodes } +
|
||||
langs.filterNot { it.code in activeLangsCodes }
|
||||
val orderedLangs = sourcesByLang.keys.filter { it in activeLangsCodes } +
|
||||
sourcesByLang.keys.filterNot { it in activeLangsCodes }
|
||||
|
||||
orderedLangs.forEach { lang ->
|
||||
val sources = sourcesByLang[lang].orEmpty().sortedBy { it.name }
|
||||
|
||||
// Create a preference group and set initial state and change listener
|
||||
SwitchPreferenceCategory(context).apply {
|
||||
preferenceScreen.addPreference(this)
|
||||
title = lang.lang
|
||||
title = Locale(lang).let { it.getDisplayLanguage(it).capitalize() }
|
||||
isPersistent = false
|
||||
if (lang.code in activeLangsCodes) {
|
||||
if (lang in activeLangsCodes) {
|
||||
setChecked(true)
|
||||
addLanguageSources(this)
|
||||
addLanguageSources(this, sources)
|
||||
}
|
||||
|
||||
setOnPreferenceChangeListener { preference, any ->
|
||||
val checked = any as Boolean
|
||||
val current = preferences.enabledLanguages().getOrDefault()
|
||||
if (!checked) {
|
||||
preferences.enabledLanguages().set(current - lang.code)
|
||||
preferences.enabledLanguages().set(current - lang)
|
||||
removeAll()
|
||||
} else {
|
||||
preferences.enabledLanguages().set(current + lang.code)
|
||||
addLanguageSources(this)
|
||||
preferences.enabledLanguages().set(current + lang)
|
||||
addLanguageSources(this, sources)
|
||||
}
|
||||
true
|
||||
}
|
||||
@ -84,8 +87,7 @@ class SettingsSourcesFragment : SettingsFragment() {
|
||||
*
|
||||
* @param group the language category.
|
||||
*/
|
||||
private fun addLanguageSources(group: SwitchPreferenceCategory) {
|
||||
val sources = onlineSources.filter { it.lang.lang == group.title }.sortedBy { it.name }
|
||||
private fun addLanguageSources(group: SwitchPreferenceCategory, sources: List<OnlineSource>) {
|
||||
val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault()
|
||||
|
||||
sources.forEach { source ->
|
||||
|
@ -1,89 +1,94 @@
|
||||
package eu.kanade.tachiyomi.ui.setting
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v7.preference.PreferenceCategory
|
||||
import android.support.v7.preference.XpPreferenceFragment
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.widget.preference.LoginPreference
|
||||
import eu.kanade.tachiyomi.widget.preference.MangaSyncLoginDialog
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class SettingsSyncFragment : SettingsFragment() {
|
||||
|
||||
companion object {
|
||||
const val SYNC_CHANGE_REQUEST = 121
|
||||
|
||||
fun newInstance(rootKey: String): SettingsSyncFragment {
|
||||
val args = Bundle()
|
||||
args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey)
|
||||
return SettingsSyncFragment().apply { arguments = args }
|
||||
}
|
||||
}
|
||||
|
||||
private val syncManager: MangaSyncManager by injectLazy()
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
val syncCategory: PreferenceCategory by bindPref(R.string.pref_category_manga_sync_accounts_key)
|
||||
|
||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||
super.onViewCreated(view, savedState)
|
||||
|
||||
registerService(syncManager.myAnimeList)
|
||||
|
||||
// registerService(syncManager.aniList) {
|
||||
// val intent = CustomTabsIntent.Builder()
|
||||
// .setToolbarColor(activity.theme.getResourceColor(R.attr.colorPrimary))
|
||||
// .build()
|
||||
// intent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||
// intent.launchUrl(activity, AnilistApi.authUrl())
|
||||
// }
|
||||
}
|
||||
|
||||
private fun <T : MangaSyncService> registerService(
|
||||
service: T,
|
||||
onPreferenceClick: (T) -> Unit = defaultOnPreferenceClick) {
|
||||
|
||||
LoginPreference(preferenceManager.context).apply {
|
||||
key = preferences.keys.syncUsername(service.id)
|
||||
title = service.name
|
||||
|
||||
setOnPreferenceClickListener {
|
||||
onPreferenceClick(service)
|
||||
true
|
||||
}
|
||||
|
||||
syncCategory.addPreference(this)
|
||||
}
|
||||
}
|
||||
|
||||
private val defaultOnPreferenceClick: (MangaSyncService) -> Unit
|
||||
get() = {
|
||||
val fragment = MangaSyncLoginDialog.newInstance(it)
|
||||
fragment.setTargetFragment(this, SYNC_CHANGE_REQUEST)
|
||||
fragment.show(fragmentManager, null)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// Manually refresh anilist holder
|
||||
// updatePreference(syncManager.aniList.id)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == SYNC_CHANGE_REQUEST) {
|
||||
updatePreference(resultCode)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePreference(id: Int) {
|
||||
val pref = findPreference(preferences.keys.syncUsername(id)) as? LoginPreference
|
||||
pref?.notifyChanged()
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.setting
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.customtabs.CustomTabsIntent
|
||||
import android.support.v7.preference.PreferenceCategory
|
||||
import android.support.v7.preference.XpPreferenceFragment
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import eu.kanade.tachiyomi.widget.preference.LoginPreference
|
||||
import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class SettingsTrackingFragment : SettingsFragment() {
|
||||
|
||||
companion object {
|
||||
const val SYNC_CHANGE_REQUEST = 121
|
||||
|
||||
fun newInstance(rootKey: String): SettingsTrackingFragment {
|
||||
val args = Bundle()
|
||||
args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey)
|
||||
return SettingsTrackingFragment().apply { arguments = args }
|
||||
}
|
||||
}
|
||||
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
val syncCategory: PreferenceCategory by bindPref(R.string.pref_category_tracking_accounts_key)
|
||||
|
||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||
super.onViewCreated(view, savedState)
|
||||
|
||||
registerService(trackManager.myAnimeList)
|
||||
|
||||
registerService(trackManager.aniList) {
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setToolbarColor(activity.theme.getResourceColor(R.attr.colorPrimary))
|
||||
.build()
|
||||
intent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||
intent.launchUrl(activity, AnilistApi.authUrl())
|
||||
}
|
||||
|
||||
registerService(trackManager.kitsu)
|
||||
}
|
||||
|
||||
private fun <T : TrackService> registerService(
|
||||
service: T,
|
||||
onPreferenceClick: (T) -> Unit = defaultOnPreferenceClick) {
|
||||
|
||||
LoginPreference(preferenceManager.context).apply {
|
||||
key = preferences.keys.trackUsername(service.id)
|
||||
title = service.name
|
||||
|
||||
setOnPreferenceClickListener {
|
||||
onPreferenceClick(service)
|
||||
true
|
||||
}
|
||||
|
||||
syncCategory.addPreference(this)
|
||||
}
|
||||
}
|
||||
|
||||
private val defaultOnPreferenceClick: (TrackService) -> Unit
|
||||
get() = {
|
||||
val fragment = TrackLoginDialog.newInstance(it)
|
||||
fragment.setTargetFragment(this, SYNC_CHANGE_REQUEST)
|
||||
fragment.show(fragmentManager, null)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// Manually refresh anilist holder
|
||||
updatePreference(trackManager.aniList.id)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == SYNC_CHANGE_REQUEST) {
|
||||
updatePreference(resultCode)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePreference(id: Int) {
|
||||
val pref = findPreference(preferences.keys.trackUsername(id)) as? LoginPreference
|
||||
pref?.notifyChanged()
|
||||
}
|
||||
|
||||
}
|
@ -44,6 +44,11 @@ fun syncChaptersWithSource(db: DatabaseHelper,
|
||||
// Chapters from the db not in the source.
|
||||
val toDelete = dbChapters.filterNot { it in sourceChapters }
|
||||
|
||||
// Return if there's nothing to add or delete, avoiding unnecessary db transactions.
|
||||
if (toAdd.isEmpty() && toDelete.isEmpty()) {
|
||||
return Pair(emptyList(), emptyList())
|
||||
}
|
||||
|
||||
val readded = mutableListOf<Chapter>()
|
||||
|
||||
db.inTransaction {
|
||||
|
@ -3,45 +3,116 @@ package eu.kanade.tachiyomi.util
|
||||
import android.app.Application
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.LocaleList
|
||||
import android.view.ContextThemeWrapper
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Locale
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Utility class to change the application's language in runtime.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
object LocaleHelper {
|
||||
|
||||
/**
|
||||
* Preferences helper.
|
||||
*/
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private var pLocale = Locale(intToLangCode(preferences.lang()))
|
||||
/**
|
||||
* The system's locale.
|
||||
*/
|
||||
private var systemLocale: Locale? = null
|
||||
|
||||
fun setLocale(locale: Locale) {
|
||||
pLocale = locale
|
||||
Locale.setDefault(pLocale)
|
||||
/**
|
||||
* The application's locale. When it's null, the system locale is used.
|
||||
*/
|
||||
private var appLocale = getLocaleFromString(preferences.lang())
|
||||
|
||||
/**
|
||||
* The currently applied locale. Used to avoid losing the selected language after a non locale
|
||||
* configuration change to the application.
|
||||
*/
|
||||
private var currentLocale: Locale? = null
|
||||
|
||||
/**
|
||||
* Returns the locale for the value stored in preferences, or null if it's system language.
|
||||
*
|
||||
* @param pref the string value stored in preferences.
|
||||
*/
|
||||
fun getLocaleFromString(pref: String): Locale? {
|
||||
if (pref.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
val parts = pref.split("_", "-")
|
||||
val lang = parts[0]
|
||||
val country = parts.getOrNull(1) ?: ""
|
||||
return Locale(lang, country)
|
||||
}
|
||||
|
||||
fun updateCfg(wrapper: ContextThemeWrapper) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
val config = Configuration()
|
||||
config.setLocale(pLocale)
|
||||
/**
|
||||
* Changes the application's locale with a new preference.
|
||||
*
|
||||
* @param pref the new value stored in preferences.
|
||||
*/
|
||||
fun changeLocale(pref: String) {
|
||||
appLocale = getLocaleFromString(pref)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the app's language to an activity.
|
||||
*/
|
||||
fun updateConfiguration(wrapper: ContextThemeWrapper) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && appLocale != null) {
|
||||
val config = Configuration(preferences.context.resources.configuration)
|
||||
config.setLocale(appLocale)
|
||||
wrapper.applyOverrideConfiguration(config)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCfg(app: Application, config: Configuration) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
config.locale = pLocale
|
||||
app.baseContext.resources.updateConfiguration(config, app.baseContext.resources.displayMetrics)
|
||||
/**
|
||||
* Updates the app's language to the application.
|
||||
*/
|
||||
fun updateConfiguration(app: Application, config: Configuration, configChange: Boolean = false) {
|
||||
if (systemLocale == null) {
|
||||
systemLocale = getConfigLocale(config)
|
||||
}
|
||||
if (configChange) {
|
||||
val configLocale = getConfigLocale(config)
|
||||
if (currentLocale == configLocale) {
|
||||
return
|
||||
}
|
||||
systemLocale = configLocale
|
||||
}
|
||||
currentLocale = appLocale ?: systemLocale ?: Locale.getDefault()
|
||||
val newConfig = updateConfigLocale(config, currentLocale!!)
|
||||
val resources = app.resources
|
||||
resources.updateConfiguration(newConfig, resources.displayMetrics)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locale applied in the given configuration.
|
||||
*/
|
||||
private fun getConfigLocale(config: Configuration): Locale {
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
config.locale
|
||||
} else {
|
||||
config.locales[0]
|
||||
}
|
||||
}
|
||||
|
||||
fun intToLangCode(i: Int): String {
|
||||
return when(i) {
|
||||
1 -> "en"
|
||||
2 -> "es"
|
||||
3 -> "it"
|
||||
4 -> "pt"
|
||||
else -> "" // System Language
|
||||
/**
|
||||
* Returns a new configuration with the given locale applied.
|
||||
*/
|
||||
private fun updateConfigLocale(config: Configuration, locale: Locale): Configuration {
|
||||
val newConfig = Configuration(config)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
newConfig.locale = locale
|
||||
} else {
|
||||
newConfig.locales = LocaleList(locale)
|
||||
}
|
||||
return newConfig
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ class SourceLoginDialog : LoginDialogPreference() {
|
||||
requestSubscription?.unsubscribe()
|
||||
|
||||
v?.apply {
|
||||
if (username.text.length == 0 || password.text.length == 0)
|
||||
if (username.text.isEmpty() || password.text.isEmpty())
|
||||
return
|
||||
|
||||
login.progress = 1
|
||||
@ -69,6 +69,7 @@ class SourceLoginDialog : LoginDialogPreference() {
|
||||
}, { error ->
|
||||
login.progress = -1
|
||||
login.setText(R.string.unknown_error)
|
||||
error.message?.let { context.toast(it) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -3,20 +3,20 @@ package eu.kanade.tachiyomi.widget.preference
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import kotlinx.android.synthetic.main.pref_account_login.view.*
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MangaSyncLoginDialog : LoginDialogPreference() {
|
||||
class TrackLoginDialog : LoginDialogPreference() {
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(sync: MangaSyncService): LoginDialogPreference {
|
||||
val fragment = MangaSyncLoginDialog()
|
||||
fun newInstance(sync: TrackService): LoginDialogPreference {
|
||||
val fragment = TrackLoginDialog()
|
||||
val bundle = Bundle(1)
|
||||
bundle.putInt("key", sync.id)
|
||||
fragment.arguments = bundle
|
||||
@ -24,15 +24,15 @@ class MangaSyncLoginDialog : LoginDialogPreference() {
|
||||
}
|
||||
}
|
||||
|
||||
val syncManager: MangaSyncManager by injectLazy()
|
||||
val trackManager: TrackManager by injectLazy()
|
||||
|
||||
lateinit var sync: MangaSyncService
|
||||
lateinit var sync: TrackService
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val syncId = arguments.getInt("key")
|
||||
sync = syncManager.getService(syncId)!!
|
||||
sync = trackManager.getService(syncId)!!
|
||||
}
|
||||
|
||||
override fun setCredentialsOnView(view: View) = with(view) {
|
||||
@ -45,7 +45,7 @@ class MangaSyncLoginDialog : LoginDialogPreference() {
|
||||
requestSubscription?.unsubscribe()
|
||||
|
||||
v?.apply {
|
||||
if (username.text.length == 0 || password.text.length == 0)
|
||||
if (username.text.isEmpty() || password.text.isEmpty())
|
||||
return
|
||||
|
||||
login.progress = 1
|
||||
@ -56,13 +56,12 @@ class MangaSyncLoginDialog : LoginDialogPreference() {
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({
|
||||
sync.saveCredentials(user, pass)
|
||||
dialog.dismiss()
|
||||
context.toast(R.string.login_success)
|
||||
}, { error ->
|
||||
sync.logout()
|
||||
login.progress = -1
|
||||
login.setText(R.string.unknown_error)
|
||||
error.message?.let { context.toast(it) }
|
||||
})
|
||||
|
||||
}
|
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>
|
@ -25,8 +25,9 @@
|
||||
android:id="@+id/tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/Theme.ActionBar.Tab.Filled"
|
||||
app:tabIndicatorColor="@android:color/white" />
|
||||
android:theme="@style/Theme.ActionBar.Tab"
|
||||
app:tabGravity="fill"
|
||||
app:tabIndicatorColor="@android:color/white"/>
|
||||
|
||||
</android.support.design.widget.AppBarLayout>
|
||||
|
||||
|
@ -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>
|
@ -10,6 +10,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:descendantFocusability="blocksDescendants"
|
||||
app:max="10"
|
||||
app:min="0"/>
|
||||
|
@ -14,11 +14,11 @@
|
||||
android:paddingRight="@dimen/margin_right">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/myanimelist_search_field"
|
||||
android:id="@+id/track_search"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/title_hint"/>
|
||||
android:hint="@string/title"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
android:visibility="gone"/>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/myanimelist_search_results"
|
||||
android:id="@+id/track_search_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:choiceMode="singleChoice"
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user