Compare commits

...

28 Commits

Author SHA1 Message Date
len
9bcde69ee0 Release 0.4.2 2017-01-01 21:00:52 +01:00
len
beca2b429c Minor changes 2017-01-01 20:54:41 +01:00
len
3a1699f0b3 Fix #373 and a few crashes 2016-12-31 16:19:32 +01:00
len
a7192e866f Locale fix. Kotlin update to 1.0.6 2016-12-27 20:18:38 +01:00
len
dc882b4dce Make clear error codes are from HTTP 2016-12-26 18:12:15 +01:00
len
77b4de3941 Minor changes 2016-12-26 17:21:17 +01:00
len
006d17aac7 Fix locale not applied outside activities 2016-12-26 16:56:19 +01:00
len
1a3a1db4ff Remove Language class. App's language and hidden languages settings were reset 2016-12-26 15:44:59 +01:00
len
97fa659283 Kitsu fixes 2016-12-24 11:32:45 +01:00
f1d84ccb49 Add "Completed" filter; fix Mangahere; fix Mangafox (#604)
* Add "Compled" filter to all english sources; fix Mangahere manga title extraction; fix Mangafox search.

* update Mangasee

* update Batoto
2016-12-24 00:08:49 +01:00
1f14240251 Translated some strings to Italian (#602)
* Translated some strings to Italian

* Added missing strings and fixed a couple of errors
2016-12-24 00:08:27 +01:00
len
ea6fed6ecf Exclude novels from Kitsu results 2016-12-23 16:58:36 +01:00
len
d09eca7833 Anilist/Kitsu Fixes 2016-12-23 16:15:09 +01:00
2c6f64c5ae Refresh option in the library updates tab (#606)
* Solves #550

* Make sure only refresh can only happen when pulling down at top of update library list

* Removed unused import
2016-12-23 15:58:53 +01:00
ec87e4359b Drawerfix/readme update (#601)
* Fixed back button on navigational drawers

* Removing an unused import

* Cleaned up code

* little clean up
2016-12-23 15:56:10 +01:00
len
2b63bae989 Show login errors 2016-12-22 22:11:17 +01:00
len
82f4e3157a Minor changes 2016-12-22 21:57:15 +01:00
len
725ceab00b Hide API implementation from MAL service. Reorder methods and minor changes 2016-12-22 21:17:47 +01:00
len
ba428c401d Fix Kitsu refresh method 2016-12-22 16:34:34 +01:00
len
510669ee2c Fix wrong anilist decimal scores 2016-12-22 16:22:08 +01:00
len
8d749df290 Score formatting. Hide API from Anilist/Kitsu services. 2016-12-21 22:39:46 +01:00
len
091c0c0c71 Fix system language setting always using english 2016-12-21 00:42:46 +01:00
7fdd2cacd7 Fixed updater on Android N. Closes #592 (#595) 2016-12-21 00:34:31 +01:00
2241a0b2de Using title instead of text for Mangahere titles (#591)
Fixes #571
The text on the popular manga page of Mangahere contains escaped HTML characters. The title attributes of the links do not contain them.
2016-12-20 20:37:45 +01:00
len
d21a93123b Dependency updates 2016-12-20 18:58:21 +01:00
len
e542a8d8e2 Fix tab gravity 2016-12-20 16:54:56 +01:00
94ee4e7fb5 Experimental Anilist and Kitsu support (#586)
* Tracking tab with anilist support

* Rename MangaSync to Track

* Rename variables and methods to track

* Kitsu implementation

* Variables refactoring

* Travis fix?
2016-12-18 22:56:28 +01:00
len
e3d430eb5e Fix #587 2016-12-18 22:31:20 +01:00
117 changed files with 2829 additions and 1897 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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)
}
}

View File

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

View File

@ -0,0 +1,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)
}
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -0,0 +1,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 ""
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}" }
}
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,14 +10,14 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackUpdateService
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.reader.notification.ImageNotifier
import eu.kanade.tachiyomi.util.DiskUtil
@ -54,9 +54,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
val downloadManager: DownloadManager by injectLazy()
/**
* Sync manager.
* Tracking manager.
*/
val syncManager: MangaSyncManager by injectLazy()
val trackManager: TrackManager by injectLazy()
/**
* Source manager.
@ -124,7 +124,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
/**
* List of manga services linked to the active manga, or null if auto syncing is not enabled.
*/
private var mangaSyncList: List<MangaSync>? = null
private var trackList: List<Track>? = null
/**
* Chapter loader whose job is to obtain the chapter list and initialize every page.
@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,20 +3,20 @@ package eu.kanade.tachiyomi.widget.preference
import android.os.Bundle
import android.view.View
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.pref_account_login.view.*
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
class MangaSyncLoginDialog : LoginDialogPreference() {
class TrackLoginDialog : LoginDialogPreference() {
companion object {
fun newInstance(sync: MangaSyncService): LoginDialogPreference {
val fragment = MangaSyncLoginDialog()
fun newInstance(sync: TrackService): LoginDialogPreference {
val fragment = TrackLoginDialog()
val bundle = Bundle(1)
bundle.putInt("key", sync.id)
fragment.arguments = bundle
@ -24,15 +24,15 @@ class MangaSyncLoginDialog : LoginDialogPreference() {
}
}
val syncManager: MangaSyncManager by injectLazy()
val trackManager: TrackManager by injectLazy()
lateinit var sync: MangaSyncService
lateinit var sync: TrackService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val syncId = arguments.getInt("key")
sync = syncManager.getService(syncId)!!
sync = trackManager.getService(syncId)!!
}
override fun setCredentialsOnView(view: View) = with(view) {
@ -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) }
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More