Преглед на файлове

Initial support for external sources

inorichi преди 8 години
родител
ревизия
dd56d7c0bb
променени са 60 файла, в които са добавени 1372 реда и са изтрити 1127 реда
  1. 11 7
      app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt
  2. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt
  3. 2 9
      app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt
  4. 6 49
      app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt
  5. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt
  6. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt
  7. 6 2
      app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
  8. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaModelLoader.kt
  9. 4 2
      app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
  10. 2 2
      app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt
  11. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
  12. 46 0
      app/src/main/java/eu/kanade/tachiyomi/data/source/CatalogueSource.kt
  13. 6 12
      app/src/main/java/eu/kanade/tachiyomi/data/source/Source.kt
  14. 115 17
      app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt
  15. 18 0
      app/src/main/java/eu/kanade/tachiyomi/data/source/model/Filter.kt
  16. 14 0
      app/src/main/java/eu/kanade/tachiyomi/data/source/model/FilterList.kt
  17. 1 11
      app/src/main/java/eu/kanade/tachiyomi/data/source/model/MangasPage.kt
  18. 28 0
      app/src/main/java/eu/kanade/tachiyomi/data/source/model/SChapter.kt
  19. 13 0
      app/src/main/java/eu/kanade/tachiyomi/data/source/model/SChapterImpl.kt
  20. 58 0
      app/src/main/java/eu/kanade/tachiyomi/data/source/model/SManga.kt
  21. 23 0
      app/src/main/java/eu/kanade/tachiyomi/data/source/model/SMangaImpl.kt
  22. 132 251
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt
  23. 98 0
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSourceFetcher.kt
  24. 64 75
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/ParsedOnlineSource.kt
  25. 69 49
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSource.kt
  26. 5 5
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSourceMappings.kt
  27. 42 87
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.kt
  28. 84 78
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.kt
  29. 42 30
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangafox.kt
  30. 40 26
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.kt
  31. 44 93
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangasee.kt
  32. 38 33
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Readmangatoday.kt
  33. 36 21
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/german/WieManga.kt
  34. 64 48
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mangachan.kt
  35. 42 24
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mintmanga.kt
  36. 44 26
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Readmanga.kt
  37. 22 13
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt
  38. 4 3
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt
  39. 17 13
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt
  40. 37 50
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt
  41. 3 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/NoResultsException.kt
  42. 14 8
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/Pager.kt
  43. 9 15
      app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPager.kt
  44. 5 5
      app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt
  45. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
  46. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt
  47. 4 3
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt
  48. 2 1
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt
  49. 14 3
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ChapterLoader.kt
  50. 3 1
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
  51. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt
  52. 4 3
      app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesFragment.kt
  53. 3 2
      app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingFragment.kt
  54. 13 6
      app/src/main/java/eu/kanade/tachiyomi/util/ChapterSourceSync.kt
  55. 0 26
      app/src/main/java/eu/kanade/tachiyomi/util/UrlUtil.java
  56. 2 1
      app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt
  57. 3 1
      app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt
  58. 2 2
      app/src/main/java/eu/kanade/tachiyomi/widget/preference/SourceLoginDialog.kt
  59. 1 1
      app/src/main/res/values/keys.xml
  60. 5 5
      app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.kt

+ 11 - 7
app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt

@@ -5,6 +5,7 @@ import android.text.format.Formatter
 import com.github.salomonbrys.kotson.fromJson
 import com.google.gson.Gson
 import com.jakewharton.disklrucache.DiskLruCache
+import eu.kanade.tachiyomi.data.database.models.Chapter
 import eu.kanade.tachiyomi.data.source.model.Page
 import eu.kanade.tachiyomi.util.DiskUtil
 import eu.kanade.tachiyomi.util.saveTo
@@ -92,13 +93,13 @@ class ChapterCache(private val context: Context) {
     /**
      * Get page list from cache.
      *
-     * @param chapterUrl the url of the chapter.
+     * @param chapter the chapter.
      * @return an observable of the list of pages.
      */
-    fun getPageListFromCache(chapterUrl: String): Observable<List<Page>> {
-        return Observable.fromCallable<List<Page>> {
+    fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> {
+        return Observable.fromCallable {
             // Get the key for the chapter.
-            val key = DiskUtil.hashKeyForDisk(chapterUrl)
+            val key = DiskUtil.hashKeyForDisk(getKey(chapter))
 
             // Convert JSON string to list of objects. Throws an exception if snapshot is null
             diskCache.get(key).use {
@@ -110,10 +111,10 @@ class ChapterCache(private val context: Context) {
     /**
      * Add page list to disk cache.
      *
-     * @param chapterUrl the url of the chapter.
+     * @param chapter the chapter.
      * @param pages list of pages.
      */
-    fun putPageListToCache(chapterUrl: String, pages: List<Page>) {
+    fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
         // Convert list of pages to json string.
         val cachedValue = gson.toJson(pages)
 
@@ -122,7 +123,7 @@ class ChapterCache(private val context: Context) {
 
         try {
             // Get editor from md5 key.
-            val key = DiskUtil.hashKeyForDisk(chapterUrl)
+            val key = DiskUtil.hashKeyForDisk(getKey(chapter))
             editor = diskCache.edit(key) ?: return
 
             // Write chapter urls to cache.
@@ -196,5 +197,8 @@ class ChapterCache(private val context: Context) {
         }
     }
 
+    private fun getKey(chapter: Chapter): String {
+        return "${chapter.manga_id}${chapter.url}"
+    }
 }
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt

@@ -69,7 +69,7 @@ open class MangaGetResolver : DefaultGetResolver<Manga>() {
 
     override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply {
         id = cursor.getLong(cursor.getColumnIndex(COL_ID))
-        source = cursor.getInt(cursor.getColumnIndex(COL_SOURCE))
+        source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
         url = cursor.getString(cursor.getColumnIndex(COL_URL))
         artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
         author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR))

+ 2 - 9
app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt

@@ -1,17 +1,14 @@
 package eu.kanade.tachiyomi.data.database.models
 
+import eu.kanade.tachiyomi.data.source.model.SChapter
 import java.io.Serializable
 
-interface Chapter : Serializable {
+interface Chapter : SChapter, Serializable {
 
     var id: Long?
 
     var manga_id: Long?
 
-    var url: String
-
-    var name: String
-
     var read: Boolean
 
     var bookmark: Boolean
@@ -20,10 +17,6 @@ interface Chapter : Serializable {
 
     var date_fetch: Long
 
-    var date_upload: Long
-
-    var chapter_number: Float
-
     var source_order: Int
 
     val isRecognizedNumber: Boolean

+ 6 - 49
app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt

@@ -1,35 +1,17 @@
 package eu.kanade.tachiyomi.data.database.models
 
-import java.io.Serializable
+import eu.kanade.tachiyomi.data.source.model.SManga
 
-interface Manga : Serializable {
+interface Manga : SManga {
 
     var id: Long?
 
-    var source: Int
-
-    var url: String
-
-    var title: String
-
-    var artist: String?
-
-    var author: String?
-
-    var description: String?
-
-    var genre: String?
-
-    var status: Int
-
-    var thumbnail_url: String?
+    var source: Long
 
     var favorite: Boolean
 
     var last_update: Long
 
-    var initialized: Boolean
-
     var viewer: Int
 
     var chapter_flags: Int
@@ -38,27 +20,6 @@ interface Manga : Serializable {
 
     var category: Int
 
-    fun copyFrom(other: Manga) {
-        if (other.author != null)
-            author = other.author
-
-        if (other.artist != null)
-            artist = other.artist
-
-        if (other.description != null)
-            description = other.description
-
-        if (other.genre != null)
-            genre = other.genre
-
-        if (other.thumbnail_url != null)
-            thumbnail_url = other.thumbnail_url
-
-        status = other.status
-
-        initialized = true
-    }
-
     fun setChapterOrder(order: Int) {
         setFlags(order, SORT_MASK)
     }
@@ -94,11 +55,6 @@ interface Manga : Serializable {
 
     companion object {
 
-        const val UNKNOWN = 0
-        const val ONGOING = 1
-        const val COMPLETED = 2
-        const val LICENSED = 3
-
         const val SORT_DESC = 0x00000000
         const val SORT_ASC = 0x00000001
         const val SORT_MASK = 0x00000001
@@ -126,12 +82,13 @@ interface Manga : Serializable {
         const val DISPLAY_NUMBER = 0x00100000
         const val DISPLAY_MASK = 0x00100000
 
-        fun create(source: Int): Manga = MangaImpl().apply {
+        fun create(source: Long): Manga = MangaImpl().apply {
             this.source = source
         }
 
-        fun create(pathUrl: String, source: Int = 0): Manga = MangaImpl().apply {
+        fun create(pathUrl: String, title: String, source: Long = 0): Manga = MangaImpl().apply {
             url = pathUrl
+            this.title = title
             this.source = source
         }
     }

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt

@@ -4,7 +4,7 @@ class MangaImpl : Manga {
 
     override var id: Long? = null
 
-    override var source: Int = 0
+    override var source: Long = 0
 
     override lateinit var url: String
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt

@@ -40,7 +40,7 @@ interface MangaQueries : DbProvider {
                     .build())
             .prepare()
 
-    fun getManga(url: String, sourceId: Int) = db.get()
+    fun getManga(url: String, sourceId: Long) = db.get()
             .`object`(Manga::class.java)
             .withQuery(Query.builder()
                     .table(MangaTable.TABLE)

+ 6 - 2
app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt

@@ -13,6 +13,7 @@ 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.source.online.fetchAllImageUrlsFromPageList
 import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
 import eu.kanade.tachiyomi.util.RetryWithDelay
 import eu.kanade.tachiyomi.util.plusAssign
@@ -251,8 +252,11 @@ class Downloader(private val context: Context, private val provider: DownloadPro
 
         val pageListObservable = if (download.pages == null) {
             // Pull page list from network and add them to download object
-            download.source.fetchPageListFromNetwork(download.chapter)
+            download.source.fetchPageList(download.chapter)
                     .doOnNext { pages ->
+                        if (pages.isEmpty()) {
+                            throw Exception("Page list is empty")
+                        }
                         download.pages = pages
                     }
         } else {
@@ -345,7 +349,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
     private fun downloadImage(page: Page, source: OnlineSource, tmpDir: UniFile, filename: String): Observable<UniFile> {
         page.status = Page.DOWNLOAD_IMAGE
         page.progress = 0
-        return source.imageResponse(page)
+        return source.fetchImage(page)
                 .map { response ->
                     val file = tmpDir.createFile("$filename.tmp")
                     try {

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaModelLoader.kt

@@ -52,7 +52,7 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
     /**
      * Map where request headers are stored for a source.
      */
-    private val cachedHeaders = hashMapOf<Int, LazyHeaders>()
+    private val cachedHeaders = hashMapOf<Long, LazyHeaders>()
 
     /**
      * Factory class for creating [MangaModelLoader] instances.

+ 4 - 2
app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt

@@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
 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.model.SManga
 import eu.kanade.tachiyomi.data.source.online.OnlineSource
 import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.util.*
@@ -214,7 +215,7 @@ class LibraryUpdateService : Service() {
         }
 
         if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) {
-            listToUpdate = listToUpdate.filter { it.status != Manga.COMPLETED }
+            listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
         }
 
         return listToUpdate
@@ -328,9 +329,10 @@ class LibraryUpdateService : Service() {
                             ?: return@concatMap Observable.empty<Manga>()
 
                     source.fetchMangaDetails(manga)
-                            .doOnNext { networkManga ->
+                            .map { networkManga ->
                                 manga.copyFrom(networkManga)
                                 db.insertManga(manga).executeAsBlocking()
+                                manga
                             }
                             .onErrorReturn { manga }
                 }

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt

@@ -91,9 +91,9 @@ class PreferenceKeys(context: Context) {
 
     val downloadNew = context.getString(R.string.pref_download_new_key)
 
-    fun sourceUsername(sourceId: Int) = "pref_source_username_$sourceId"
+    fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
 
-    fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId"
+    fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
 
     fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt

@@ -74,7 +74,7 @@ class PreferencesHelper(val context: Context) {
 
     fun askUpdateTrack() = prefs.getBoolean(keys.askUpdateTrack, false)
 
-    fun lastUsedCatalogueSource() = rxPrefs.getInteger(keys.lastUsedCatalogueSource, -1)
+    fun lastUsedCatalogueSource() = rxPrefs.getLong(keys.lastUsedCatalogueSource, -1)
 
     fun lastUsedCategory() = rxPrefs.getInteger(keys.lastUsedCategory, 0)
 

+ 46 - 0
app/src/main/java/eu/kanade/tachiyomi/data/source/CatalogueSource.kt

@@ -0,0 +1,46 @@
+package eu.kanade.tachiyomi.data.source
+
+import eu.kanade.tachiyomi.data.source.model.FilterList
+import eu.kanade.tachiyomi.data.source.model.MangasPage
+import rx.Observable
+
+interface CatalogueSource : Source {
+
+    /**
+     * An ISO 639-1 compliant language code (two letters in lower case).
+     */
+    val lang: String
+
+    /**
+     * Whether the source has support for latest updates.
+     */
+    val supportsLatest: Boolean
+
+    /**
+     * Returns an observable containing a page with a list of manga.
+     *
+     * @param page the page number to retrieve.
+     */
+    fun fetchPopularManga(page: Int): Observable<MangasPage>
+
+    /**
+     * Returns an observable containing a page with a list of manga.
+     *
+     * @param page the page number to retrieve.
+     * @param query the search query.
+     * @param filters the list of filters to apply.
+     */
+    fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage>
+
+    /**
+     * Returns an observable containing a page with a list of latest manga updates.
+     *
+     * @param page the page number to retrieve.
+     */
+    fun fetchLatestUpdates(page: Int): Observable<MangasPage>
+
+    /**
+     * Returns the list of filters for the source.
+     */
+    fun getFilterList(): FilterList
+}

+ 6 - 12
app/src/main/java/eu/kanade/tachiyomi/data/source/Source.kt

@@ -1,8 +1,8 @@
 package eu.kanade.tachiyomi.data.source
 
-import eu.kanade.tachiyomi.data.database.models.Chapter
-import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.data.source.model.SChapter
+import eu.kanade.tachiyomi.data.source.model.SManga
 import rx.Observable
 
 /**
@@ -13,7 +13,7 @@ interface Source {
     /**
      * Id for the source. Must be unique.
      */
-    val id: Int
+    val id: Long
 
     /**
      * Name of the source.
@@ -25,26 +25,20 @@ interface Source {
      *
      * @param manga the manga to update.
      */
-    fun fetchMangaDetails(manga: Manga): Observable<Manga>
+    fun fetchMangaDetails(manga: SManga): Observable<SManga>
 
     /**
      * Returns an observable with all the available chapters for a manga.
      *
      * @param manga the manga to update.
      */
-    fun fetchChapterList(manga: Manga): Observable<List<Chapter>>
+    fun fetchChapterList(manga: SManga): Observable<List<SChapter>>
 
     /**
      * Returns an observable with the list of pages a chapter has.
      *
      * @param chapter the chapter.
      */
-    fun fetchPageList(chapter: Chapter): Observable<List<Page>>
+    fun fetchPageList(chapter: SChapter): Observable<List<Page>>
 
-    /**
-     * Returns an observable with the path of the image.
-     *
-     * @param page the page.
-     */
-    fun fetchImage(page: Page): Observable<Page>
 }

+ 115 - 17
app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt

@@ -2,7 +2,10 @@ package eu.kanade.tachiyomi.data.source
 
 import android.Manifest.permission.READ_EXTERNAL_STORAGE
 import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
 import android.os.Environment
+import dalvik.system.PathClassLoader
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.source.online.OnlineSource
 import eu.kanade.tachiyomi.data.source.online.YamlOnlineSource
@@ -18,29 +21,47 @@ import java.io.File
 
 open class SourceManager(private val context: Context) {
 
-    private val sourcesMap = createSources()
+    private val sourcesMap = mutableMapOf<Long, Source>()
 
-    open fun get(sourceKey: Int): Source? {
+    init {
+        createSources()
+    }
+
+    open fun get(sourceKey: Long): Source? {
         return sourcesMap[sourceKey]
     }
 
-    fun getOnlineSources() = sourcesMap.values.filterIsInstance(OnlineSource::class.java)
+    fun getOnlineSources() = sourcesMap.values.filterIsInstance<OnlineSource>()
+
+    fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
+
+    private fun createSources() {
+        createExtensionSources().forEach { registerSource(it) }
+        createYamlSources().forEach { registerSource(it) }
+        createInternalSources().forEach { registerSource(it) }
+    }
+
+    private fun registerSource(source: Source, overwrite: Boolean = false) {
+        if (overwrite || !sourcesMap.containsKey(source.id)) {
+            sourcesMap.put(source.id, source)
+        }
+    }
 
-    private fun createOnlineSourceList(): List<Source> = listOf(
-            Batoto(1),
-            Mangahere(2),
-            Mangafox(3),
-            Kissmanga(4),
-            Readmanga(5),
-            Mintmanga(6),
-            Mangachan(7),
-            Readmangatoday(8),
-            Mangasee(9),
-            WieManga(10)
+    private fun createInternalSources(): List<Source> = listOf(
+            Batoto(),
+            Mangahere(),
+            Mangafox(),
+            Kissmanga(),
+            Readmanga(),
+            Mintmanga(),
+            Mangachan(),
+            Readmangatoday(),
+            Mangasee(),
+            WieManga()
     )
 
-    private fun createSources(): Map<Int, Source> = hashMapOf<Int, Source>().apply {
-        createOnlineSourceList().forEach { put(it.id, it) }
+    private fun createYamlSources(): List<Source> {
+        val sources = mutableListOf<Source>()
 
         val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
                 File.separator + context.getString(R.string.app_name), "parsers")
@@ -50,12 +71,89 @@ open class SourceManager(private val context: Context) {
             for (file in parsersDir.listFiles().filter { it.extension == "yml" }) {
                 try {
                     val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
-                    YamlOnlineSource(map).let { put(it.id, it) }
+                    sources.add(YamlOnlineSource(map))
                 } catch (e: Exception) {
                     Timber.e("Error loading source from file. Bad format?")
                 }
             }
         }
+        return sources
+    }
+
+    private fun createExtensionSources(): List<OnlineSource> {
+        val pkgManager = context.packageManager
+        val flags = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
+        val installedPkgs = pkgManager.getInstalledPackages(flags)
+        val extPkgs = installedPkgs.filter { it.reqFeatures.orEmpty().any { it.name == FEATURE } }
+
+        val sources = mutableListOf<OnlineSource>()
+        for (pkgInfo in extPkgs) {
+            val appInfo = pkgManager.getApplicationInfo(pkgInfo.packageName,
+                    PackageManager.GET_META_DATA) ?: continue
+
+
+            val data = appInfo.metaData
+            val extName = data.getString(NAME)
+            val version = data.getInt(VERSION)
+            val sourceClass = extendClassName(data.getString(SOURCE), pkgInfo.packageName)
+
+            val ext = Extension(extName, appInfo, version, sourceClass)
+            if (!validateExtension(ext)) {
+                continue
+            }
+
+            val instance = loadExtension(ext, pkgManager)
+            if (instance == null) {
+                Timber.e("Extension error: failed to instance $extName")
+                continue
+            }
+            sources.add(instance)
+        }
+        return sources
+    }
+
+    private fun validateExtension(ext: Extension): Boolean {
+        if (ext.version < LIB_VERSION_MIN || ext.version > LIB_VERSION_MAX) {
+            Timber.e("Extension error: ${ext.name} has version ${ext.version}, while only versions "
+                    + "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed")
+            return false
+        }
+        return true
+    }
+
+    private fun loadExtension(ext: Extension, pkgManager: PackageManager): OnlineSource? {
+        return try {
+            val classLoader = PathClassLoader(ext.appInfo.sourceDir, null, context.classLoader)
+            val resources = pkgManager.getResourcesForApplication(ext.appInfo)
+
+            Class.forName(ext.sourceClass, false, classLoader).newInstance() as? OnlineSource
+        } catch (e: Exception) {
+            null
+        } catch (e: LinkageError) {
+            null
+        }
+    }
+
+    private fun extendClassName(className: String, packageName: String): String {
+        return if (className.startsWith(".")) {
+            packageName + className
+        } else {
+            className
+        }
+    }
+
+    class Extension(val name: String,
+                    val appInfo: ApplicationInfo,
+                    val version: Int,
+                    val sourceClass: String)
+
+    private companion object {
+        const val FEATURE = "tachiyomi.extension"
+        const val NAME = "tachiyomi.extension.name"
+        const val VERSION = "tachiyomi.extension.version"
+        const val SOURCE = "tachiyomi.extension.source"
+        const val LIB_VERSION_MIN = 1
+        const val LIB_VERSION_MAX = 1
     }
 
 }

+ 18 - 0
app/src/main/java/eu/kanade/tachiyomi/data/source/model/Filter.kt

@@ -0,0 +1,18 @@
+package eu.kanade.tachiyomi.data.source.model
+
+sealed class Filter<T>(val name: String, var state: T) {
+    open class Header(name: String) : Filter<Any>(name, 0)
+    abstract class List<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state)
+    abstract class Text(name: String, state: String = "") : Filter<String>(name, state)
+    abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state)
+    abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) {
+        fun isIgnored() = state == STATE_IGNORE
+        fun isIncluded() = state == STATE_INCLUDE
+        fun isExcluded() = state == STATE_EXCLUDE
+        companion object {
+            const val STATE_IGNORE = 0
+            const val STATE_INCLUDE = 1
+            const val STATE_EXCLUDE = 2
+        }
+    }
+}

+ 14 - 0
app/src/main/java/eu/kanade/tachiyomi/data/source/model/FilterList.kt

@@ -0,0 +1,14 @@
+package eu.kanade.tachiyomi.data.source.model
+
+class FilterList(list: List<Filter<*>>) : List<Filter<*>> by list {
+
+    constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
+
+    fun hasSameState(other: FilterList): Boolean {
+        if (size != other.size) return false
+
+        return (0..lastIndex)
+                .all { get(it).javaClass == other[it].javaClass && get(it).state == other[it].state }
+    }
+
+}

+ 1 - 11
app/src/main/java/eu/kanade/tachiyomi/data/source/model/MangasPage.kt

@@ -1,13 +1,3 @@
 package eu.kanade.tachiyomi.data.source.model
 
-import eu.kanade.tachiyomi.data.database.models.Manga
-
-class MangasPage(val page: Int) {
-
-    val mangas: MutableList<Manga> = mutableListOf()
-
-    lateinit var url: String
-
-    var nextPageUrl: String? = null
-
-}
+data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)

+ 28 - 0
app/src/main/java/eu/kanade/tachiyomi/data/source/model/SChapter.kt

@@ -0,0 +1,28 @@
+package eu.kanade.tachiyomi.data.source.model
+
+import java.io.Serializable
+
+interface SChapter : Serializable {
+
+    var url: String
+
+    var name: String
+
+    var date_upload: Long
+
+    var chapter_number: Float
+
+    fun copyFrom(other: SChapter) {
+        name = other.name
+        url = other.url
+        date_upload = other.date_upload
+        chapter_number = other.chapter_number
+    }
+
+    companion object {
+        fun create(): SChapter {
+            return SChapterImpl()
+        }
+    }
+
+}

+ 13 - 0
app/src/main/java/eu/kanade/tachiyomi/data/source/model/SChapterImpl.kt

@@ -0,0 +1,13 @@
+package eu.kanade.tachiyomi.data.source.model
+
+class SChapterImpl : SChapter {
+
+    override lateinit var url: String
+
+    override lateinit var name: String
+
+    override var date_upload: Long = 0
+
+    override var chapter_number: Float = -1f
+
+}

+ 58 - 0
app/src/main/java/eu/kanade/tachiyomi/data/source/model/SManga.kt

@@ -0,0 +1,58 @@
+package eu.kanade.tachiyomi.data.source.model
+
+import java.io.Serializable
+
+interface SManga : Serializable {
+
+    var url: String
+
+    var title: String
+
+    var artist: String?
+
+    var author: String?
+
+    var description: String?
+
+    var genre: String?
+
+    var status: Int
+
+    var thumbnail_url: String?
+
+    var initialized: Boolean
+
+    fun copyFrom(other: SManga) {
+        if (other.author != null)
+            author = other.author
+
+        if (other.artist != null)
+            artist = other.artist
+
+        if (other.description != null)
+            description = other.description
+
+        if (other.genre != null)
+            genre = other.genre
+
+        if (other.thumbnail_url != null)
+            thumbnail_url = other.thumbnail_url
+
+        status = other.status
+
+        if (!initialized)
+            initialized = other.initialized
+    }
+
+    companion object {
+        const val UNKNOWN = 0
+        const val ONGOING = 1
+        const val COMPLETED = 2
+        const val LICENSED = 3
+
+        fun create(): SManga {
+            return SMangaImpl()
+        }
+    }
+
+}

+ 23 - 0
app/src/main/java/eu/kanade/tachiyomi/data/source/model/SMangaImpl.kt

@@ -0,0 +1,23 @@
+package eu.kanade.tachiyomi.data.source.model
+
+class SMangaImpl : SManga {
+
+    override lateinit var url: String
+
+    override lateinit var title: String
+
+    override var artist: String? = null
+
+    override var author: String? = null
+
+    override var description: String? = null
+
+    override var genre: String? = null
+
+    override var status: Int = 0
+
+    override var thumbnail_url: String? = null
+
+    override var initialized: Boolean = false
+
+}

+ 132 - 251
app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt

@@ -1,40 +1,32 @@
 package eu.kanade.tachiyomi.data.source.online
 
-import android.net.Uri
-import eu.kanade.tachiyomi.data.cache.ChapterCache
-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.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.Source
-import eu.kanade.tachiyomi.data.source.model.MangasPage
-import eu.kanade.tachiyomi.data.source.model.Page
-import eu.kanade.tachiyomi.util.UrlUtil
+import eu.kanade.tachiyomi.data.source.CatalogueSource
+import eu.kanade.tachiyomi.data.source.model.*
 import okhttp3.Headers
 import okhttp3.OkHttpClient
 import okhttp3.Request
 import okhttp3.Response
 import rx.Observable
 import uy.kohesive.injekt.injectLazy
+import java.net.URI
+import java.net.URISyntaxException
+import java.security.MessageDigest
 
 /**
  * A simple implementation for sources from a website.
  */
-abstract class OnlineSource() : Source {
+abstract class OnlineSource : CatalogueSource {
 
     /**
      * Network service.
      */
     val network: NetworkHelper by injectLazy()
 
-    /**
-     * Chapter cache.
-     */
-    val chapterCache: ChapterCache by injectLazy()
-
     /**
      * Preferences helper.
      */
@@ -46,24 +38,26 @@ abstract class OnlineSource() : Source {
     abstract val baseUrl: String
 
     /**
-     * An ISO 639-1 compliant language code (two characters in lower case).
+     * Version id used to generate the source id. If the site completely changes and urls are
+     * incompatible, you may increase this value and it'll be considered as a new source.
      */
-    abstract val lang: String
+    open val versionId = 1
 
     /**
-     * Whether the source has support for latest updates.
+     * Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
+     * of the MD5 of the string: sourcename/language/versionId
+     * Note the generated id sets the sign bit to 0.
      */
-    abstract val supportsLatest: Boolean
+    override val id by lazy {
+        val key = "${name.toLowerCase()}/$lang/$versionId"
+        val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
+        (0..7).map { bytes[it].toLong() and 0xff shl 8*(7-it) }.reduce(Long::or) and Long.MAX_VALUE
+    }
 
     /**
      * Headers used for requests.
      */
-    val headers by lazy { headersBuilder().build() }
-
-    /**
-     * Genre filters.
-     */
-    val filters by lazy { getFilterList() }
+    val headers: Headers by lazy { headersBuilder().build() }
 
     /**
      * Default network client for doing requests.
@@ -87,121 +81,88 @@ abstract class OnlineSource() : Source {
      * Returns an observable containing a page with a list of manga. Normally it's not needed to
      * override this method.
      *
-     * @param page the page object where the information will be saved, like the list of manga,
-     *             the current page and the next page url.
-     */
-    open fun fetchPopularManga(page: MangasPage): Observable<MangasPage> = client
-            .newCall(popularMangaRequest(page))
-            .asObservableSuccess()
-            .map { response ->
-                popularMangaParse(response, page)
-                page
-            }
-
-    /**
-     * Returns the request for the popular manga given the page. Override only if it's needed to
-     * send different headers or request method like POST.
-     *
-     * @param page the page object.
+     * @param page the page number to retrieve.
      */
-    open protected fun popularMangaRequest(page: MangasPage): Request {
-        if (page.page == 1) {
-            page.url = popularMangaInitialUrl()
-        }
-        return GET(page.url, headers)
+    override fun fetchPopularManga(page: Int): Observable<MangasPage> {
+        return client.newCall(popularMangaRequest(page))
+                .asObservableSuccess()
+                .map { response ->
+                    popularMangaParse(response)
+                }
     }
 
     /**
-     * Returns the absolute url of the first page to popular manga.
+     * Returns the request for the popular manga given the page.
+     *
+     * @param page the page number to retrieve.
      */
-    abstract protected fun popularMangaInitialUrl(): String
+    abstract protected fun popularMangaRequest(page: Int): Request
 
     /**
-     * Parse the response from the site. It should add a list of manga and the absolute url to the
-     * next page (if it has a next one) to [page].
+     * Parses the response from the site and returns a [MangasPage] object.
      *
      * @param response the response from the site.
-     * @param page the page object to be filled.
      */
-    abstract protected fun popularMangaParse(response: Response, page: MangasPage)
+    abstract protected fun popularMangaParse(response: Response): MangasPage
 
     /**
      * Returns an observable containing a page with a list of manga. Normally it's not needed to
      * override this method.
      *
-     * @param page the page object where the information will be saved, like the list of manga,
-     *             the current page and the next page url.
-     * @param query the search query.
-     */
-    open fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter<*>>): Observable<MangasPage> = client
-            .newCall(searchMangaRequest(page, query, filters))
-            .asObservableSuccess()
-            .map { response ->
-                searchMangaParse(response, page, query, filters)
-                page
-            }
-
-    /**
-     * Returns the request for the search manga given the page. Override only if it's needed to
-     * send different headers or request method like POST.
-     *
-     * @param page the page object.
+     * @param page the page number to retrieve.
      * @param query the search query.
+     * @param filters the list of filters to apply.
      */
-    open protected fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): Request {
-        if (page.page == 1) {
-            page.url = searchMangaInitialUrl(query, filters)
-        }
-        return GET(page.url, headers)
+    override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
+        return client.newCall(searchMangaRequest(page, query, filters))
+                .asObservableSuccess()
+                .map { response ->
+                    searchMangaParse(response)
+                }
     }
 
     /**
-     * Returns the absolute url of the first page to popular manga.
+     * Returns the request for the search manga given the page.
      *
+     * @param page the page number to retrieve.
      * @param query the search query.
+     * @param filters the list of filters to apply.
      */
-    abstract protected fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String
+    abstract protected fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request
 
     /**
-     * Parse the response from the site. It should add a list of manga and the absolute url to the
-     * next page (if it has a next one) to [page].
+     * Parses the response from the site and returns a [MangasPage] object.
      *
      * @param response the response from the site.
-     * @param page the page object to be filled.
-     * @param query the search query.
      */
-    abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>)
+    abstract protected fun searchMangaParse(response: Response): MangasPage
 
     /**
-     * Returns an observable containing a page with a list of latest manga.
-     */
-    open fun fetchLatestUpdates(page: MangasPage): Observable<MangasPage> = client
-            .newCall(latestUpdatesRequest(page))
-            .asObservableSuccess()
-            .map { response ->
-                latestUpdatesParse(response, page)
-                page
-            }
-
-    /**
-     * Returns the request for latest manga given the page.
+     * Returns an observable containing a page with a list of latest manga updates.
+     *
+     * @param page the page number to retrieve.
      */
-    open protected fun latestUpdatesRequest(page: MangasPage): Request {
-        if (page.page == 1) {
-            page.url = latestUpdatesInitialUrl()
-        }
-        return GET(page.url, headers)
+    override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
+        return client.newCall(latestUpdatesRequest(page))
+                .asObservableSuccess()
+                .map { response ->
+                    latestUpdatesParse(response)
+                }
     }
 
     /**
-     * Returns the absolute url of the first page to latest manga.
+     * Returns the request for latest manga given the page.
+     *
+     * @param page the page number to retrieve.
      */
-    abstract protected fun latestUpdatesInitialUrl(): String
+    abstract protected fun latestUpdatesRequest(page: Int): Request
 
     /**
-     * Same as [popularMangaParse], but for latest manga.
+     * Parses the response from the site and returns a [MangasPage] object.
+     *
+     * @param response the response from the site.
      */
-    abstract protected fun latestUpdatesParse(response: Response, page: MangasPage)
+    abstract protected fun latestUpdatesParse(response: Response): MangasPage
 
     /**
      * Returns an observable with the updated details for a manga. Normally it's not needed to
@@ -209,33 +170,30 @@ abstract class OnlineSource() : Source {
      *
      * @param manga the manga to be updated.
      */
-    override fun fetchMangaDetails(manga: Manga): Observable<Manga> = client
-            .newCall(mangaDetailsRequest(manga))
-            .asObservableSuccess()
-            .map { response ->
-                Manga.create(manga.url, id).apply {
-                    mangaDetailsParse(response, this)
-                    initialized = true
+    override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
+        return client.newCall(mangaDetailsRequest(manga))
+                .asObservableSuccess()
+                .map { response ->
+                    mangaDetailsParse(response).apply { initialized = true }
                 }
-            }
+    }
 
     /**
-     * Returns the request for updating a manga. Override only if it's needed to override the url,
-     * send different headers or request method like POST.
+     * Returns the request for the details of a manga. Override only if it's needed to change the
+     * url, send different headers or request method like POST.
      *
      * @param manga the manga to be updated.
      */
-    open fun mangaDetailsRequest(manga: Manga): Request {
+    open fun mangaDetailsRequest(manga: SManga): Request {
         return GET(baseUrl + manga.url, headers)
     }
 
     /**
-     * Parse the response from the site. It should fill [manga].
+     * Parses the response from the site and returns the details of a manga.
      *
      * @param response the response from the site.
-     * @param manga the manga whose fields have to be filled.
      */
-    abstract protected fun mangaDetailsParse(response: Response, manga: Manga)
+    abstract protected fun mangaDetailsParse(response: Response): SManga
 
     /**
      * Returns an observable with the updated chapter list for a manga. Normally it's not needed to
@@ -243,17 +201,13 @@ abstract class OnlineSource() : Source {
      *
      * @param manga the manga to look for chapters.
      */
-    override fun fetchChapterList(manga: Manga): Observable<List<Chapter>> = client
-            .newCall(chapterListRequest(manga))
-            .asObservableSuccess()
-            .map { response ->
-                mutableListOf<Chapter>().apply {
-                    chapterListParse(response, this)
-                    if (isEmpty()) {
-                        throw Exception("No chapters found")
-                    }
+    override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
+        return client.newCall(chapterListRequest(manga))
+                .asObservableSuccess()
+                .map { response ->
+                    chapterListParse(response)
                 }
-            }
+    }
 
     /**
      * Returns the request for updating the chapter list. Override only if it's needed to override
@@ -261,68 +215,46 @@ abstract class OnlineSource() : Source {
      *
      * @param manga the manga to look for chapters.
      */
-    open protected fun chapterListRequest(manga: Manga): Request {
+    open protected fun chapterListRequest(manga: SManga): Request {
         return GET(baseUrl + manga.url, headers)
     }
 
     /**
-     * Parse the response from the site. It should fill [chapters].
+     * Parses the response from the site and returns a list of chapters.
      *
      * @param response the response from the site.
-     * @param chapters the chapter list to be filled.
-     */
-    abstract protected fun chapterListParse(response: Response, chapters: MutableList<Chapter>)
-
-    /**
-     * Returns an observable with the page list for a chapter. It tries to return the page list from
-     * the local cache, otherwise fallbacks to network calling [fetchPageListFromNetwork].
-     *
-     * @param chapter the chapter whose page list has to be fetched.
      */
-    final override fun fetchPageList(chapter: Chapter): Observable<List<Page>> = chapterCache
-            .getPageListFromCache(getChapterCacheKey(chapter))
-            .onErrorResumeNext { fetchPageListFromNetwork(chapter) }
+    abstract protected fun chapterListParse(response: Response): List<SChapter>
 
     /**
-     * Returns an observable with the page list for a chapter. Normally it's not needed to override
-     * this method.
+     * Returns an observable with the page list for a chapter.
      *
      * @param chapter the chapter whose page list has to be fetched.
      */
-    open fun fetchPageListFromNetwork(chapter: Chapter): Observable<List<Page>> = client
-            .newCall(pageListRequest(chapter))
-            .asObservableSuccess()
-            .map { response ->
-                mutableListOf<Page>().apply {
-                    pageListParse(response, this)
-                    if (isEmpty()) {
-                        throw Exception("Page list is empty")
-                    }
+    override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
+        return client.newCall(pageListRequest(chapter))
+                .asObservableSuccess()
+                .map { response ->
+                    pageListParse(response)
                 }
-            }
+    }
 
     /**
      * Returns the request for getting the page list. Override only if it's needed to override the
      * url, send different headers or request method like POST.
      *
-     * @param chapter the chapter whose page list has to be fetched
+     * @param chapter the chapter whose page list has to be fetched.
      */
-    open protected fun pageListRequest(chapter: Chapter): Request {
+    open protected fun pageListRequest(chapter: SChapter): Request {
         return GET(baseUrl + chapter.url, headers)
     }
 
     /**
-     * Parse the response from the site. It should fill [pages].
+     * Parses the response from the site and returns a list of pages.
      *
      * @param response the response from the site.
-     * @param pages the page list to be filled.
      */
-    abstract protected fun pageListParse(response: Response, pages: MutableList<Page>)
-
-    /**
-     * Returns the key for the page list to be stored in [ChapterCache].
-     */
-    private fun getChapterCacheKey(chapter: Chapter) = "$id${chapter.url}"
+    abstract protected fun pageListParse(response: Response): List<Page>
 
     /**
      * Returns an observable with the page containing the source url of the image. If there's any
@@ -330,16 +262,10 @@ abstract class OnlineSource() : Source {
      *
      * @param page the page whose source image has to be fetched.
      */
-    open protected fun fetchImageUrl(page: Page): Observable<Page> {
-        page.status = Page.LOAD_PAGE
-        return client
-                .newCall(imageUrlRequest(page))
+    open fun fetchImageUrl(page: Page): Observable<String> {
+        return client.newCall(imageUrlRequest(page))
                 .asObservableSuccess()
                 .map { imageUrlParse(it) }
-                .doOnError { page.status = Page.ERROR }
-                .onErrorReturn { null }
-                .doOnNext { page.imageUrl = it }
-                .map { page }
     }
 
     /**
@@ -353,31 +279,21 @@ abstract class OnlineSource() : Source {
     }
 
     /**
-     * Parse the response from the site. It should return the absolute url to the source image.
+     * Parses the response from the site and returns the absolute url to the source image.
      *
      * @param response the response from the site.
      */
     abstract protected fun imageUrlParse(response: Response): String
 
-    /**
-     * Returns an observable of the page with the downloaded image.
-     *
-     * @param page the page whose source image has to be downloaded.
-     */
-    final override fun fetchImage(page: Page): Observable<Page> =
-            if (page.imageUrl.isNullOrEmpty())
-                fetchImageUrl(page).flatMap { getCachedImage(it) }
-            else
-                getCachedImage(page)
-
     /**
      * Returns an observable with the response of the source image.
      *
      * @param page the page whose source image has to be downloaded.
      */
-    fun imageResponse(page: Page): Observable<Response> = client
-            .newCallWithProgress(imageRequest(page), page)
-            .asObservableSuccess()
+    fun fetchImage(page: Page): Observable<Response> {
+        return client.newCallWithProgress(imageRequest(page), page)
+                .asObservableSuccess()
+    }
 
     /**
      * Returns the request for getting the source image. Override only if it's needed to override
@@ -390,68 +306,44 @@ abstract class OnlineSource() : Source {
     }
 
     /**
-     * Returns an observable of the page that gets the image from the chapter or fallbacks to
-     * network and copies it to the cache calling [cacheImage].
+     * Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
+     * database and the urls could still work after a domain change.
      *
-     * @param page the page.
-     */
-    fun getCachedImage(page: Page): Observable<Page> {
-        val imageUrl = page.imageUrl ?: return Observable.just(page)
-
-        return Observable.just(page)
-                .flatMap {
-                    if (!chapterCache.isImageInCache(imageUrl)) {
-                        cacheImage(page)
-                    } else {
-                        Observable.just(page)
-                    }
-                }
-                .doOnNext {
-                    page.uri = Uri.fromFile(chapterCache.getImageFile(imageUrl))
-                    page.status = Page.READY
-                }
-                .doOnError { page.status = Page.ERROR }
-                .onErrorReturn { page }
+     * @param url the full url to the chapter.
+     */
+    fun SChapter.setUrlWithoutDomain(url: String) {
+        this.url = getUrlWithoutDomain(url)
     }
 
     /**
-     * Returns an observable of the page that downloads the image to [ChapterCache].
+     * Assigns the url of the manga without the scheme and domain. It saves some redundancy from
+     * database and the urls could still work after a domain change.
      *
-     * @param page the page.
+     * @param url the full url to the manga.
      */
-    private fun cacheImage(page: Page): Observable<Page> {
-        page.status = Page.DOWNLOAD_IMAGE
-        return imageResponse(page)
-                .doOnNext { chapterCache.putImageToCache(page.imageUrl!!, it) }
-                .map { page }
+    fun SManga.setUrlWithoutDomain(url: String) {
+        this.url = getUrlWithoutDomain(url)
     }
 
-
-    // Utility methods
-
-    fun fetchAllImageUrlsFromPageList(pages: List<Page>) = Observable.from(pages)
-            .filter { !it.imageUrl.isNullOrEmpty() }
-            .mergeWith(fetchRemainingImageUrlsFromPageList(pages))
-
-    fun fetchRemainingImageUrlsFromPageList(pages: List<Page>) = Observable.from(pages)
-            .filter { it.imageUrl.isNullOrEmpty() }
-            .concatMap { fetchImageUrl(it) }
-
-    fun savePageList(chapter: Chapter, pages: List<Page>?) {
-        if (pages != null) {
-            chapterCache.putPageListToCache(getChapterCacheKey(chapter), pages)
+    /**
+     * Returns the url of the given string without the scheme and domain.
+     *
+     * @param orig the full url.
+     */
+    private fun getUrlWithoutDomain(orig: String): String {
+        try {
+            val uri = URI(orig)
+            var out = uri.path
+            if (uri.query != null)
+                out += "?" + uri.query
+            if (uri.fragment != null)
+                out += "#" + uri.fragment
+            return out
+        } catch (e: URISyntaxException) {
+            return orig
         }
     }
 
-    fun Chapter.setUrlWithoutDomain(url: String) {
-        this.url = UrlUtil.getPath(url)
-    }
-
-    fun Manga.setUrlWithoutDomain(url: String) {
-        this.url = UrlUtil.getPath(url)
-    }
-
-
     /**
      * Called before inserting a new chapter into database. Use it if you need to override chapter
      * fields, like the title or the chapter number. Do not change anything to [manga].
@@ -459,22 +351,11 @@ abstract class OnlineSource() : Source {
      * @param chapter the chapter to be added.
      * @param manga the manga of the chapter.
      */
-    open fun prepareNewChapter(chapter: Chapter, manga: Manga) {
+    open fun prepareNewChapter(chapter: SChapter, manga: SManga) {
     }
 
-    sealed class Filter<T>(val name: String, var state: T) {
-        open class Header(name: String) : Filter<Any>(name, 0)
-        abstract class List<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state)
-        abstract class Text(name: String, state: String = "") : Filter<String>(name, state)
-        abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state)
-        abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) {
-            companion object {
-                const val STATE_IGNORE = 0
-                const val STATE_INCLUDE = 1
-                const val STATE_EXCLUDE = 2
-            }
-        }
-    }
-
-    open fun getFilterList(): List<Filter<*>> = emptyList()
+    /**
+     * Returns the list of filters for the source.
+     */
+    override fun getFilterList() = FilterList()
 }

+ 98 - 0
app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSourceFetcher.kt

@@ -0,0 +1,98 @@
+package eu.kanade.tachiyomi.data.source.online
+
+import android.net.Uri
+import eu.kanade.tachiyomi.data.cache.ChapterCache
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.source.model.Page
+import rx.Observable
+import uy.kohesive.injekt.injectLazy
+
+
+// TODO: this should be handled with a different approach.
+
+/**
+ * Chapter cache.
+ */
+private val chapterCache: ChapterCache by injectLazy()
+
+/**
+ * Returns an observable with the page list for a chapter. It tries to return the page list from
+ * the local cache, otherwise fallbacks to network.
+ *
+ * @param chapter the chapter whose page list has to be fetched.
+ */
+fun OnlineSource.fetchPageListFromCacheThenNet(chapter: Chapter): Observable<List<Page>> {
+    return chapterCache
+            .getPageListFromCache(chapter)
+            .onErrorResumeNext { fetchPageList(chapter) }
+}
+
+/**
+ * Returns an observable of the page with the downloaded image.
+ *
+ * @param page the page whose source image has to be downloaded.
+ */
+fun OnlineSource.fetchImageFromCacheThenNet(page: Page): Observable<Page> {
+    return if (page.imageUrl.isNullOrEmpty())
+        getImageUrl(page).flatMap { getCachedImage(it) }
+    else
+        getCachedImage(page)
+}
+
+fun OnlineSource.getImageUrl(page: Page): Observable<Page> {
+    page.status = Page.LOAD_PAGE
+    return fetchImageUrl(page)
+            .doOnError { page.status = Page.ERROR }
+            .onErrorReturn { null }
+            .doOnNext { page.imageUrl = it }
+            .map { page }
+}
+
+/**
+ * Returns an observable of the page that gets the image from the chapter or fallbacks to
+ * network and copies it to the cache calling [cacheImage].
+ *
+ * @param page the page.
+ */
+fun OnlineSource.getCachedImage(page: Page): Observable<Page> {
+    val imageUrl = page.imageUrl ?: return Observable.just(page)
+
+    return Observable.just(page)
+            .flatMap {
+                if (!chapterCache.isImageInCache(imageUrl)) {
+                    cacheImage(page)
+                } else {
+                    Observable.just(page)
+                }
+            }
+            .doOnNext {
+                page.uri = Uri.fromFile(chapterCache.getImageFile(imageUrl))
+                page.status = Page.READY
+            }
+            .doOnError { page.status = Page.ERROR }
+            .onErrorReturn { page }
+}
+
+/**
+ * Returns an observable of the page that downloads the image to [ChapterCache].
+ *
+ * @param page the page.
+ */
+private fun OnlineSource.cacheImage(page: Page): Observable<Page> {
+    page.status = Page.DOWNLOAD_IMAGE
+    return fetchImage(page)
+            .doOnNext { chapterCache.putImageToCache(page.imageUrl!!, it) }
+            .map { page }
+}
+
+fun OnlineSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
+    return Observable.from(pages)
+            .filter { !it.imageUrl.isNullOrEmpty() }
+            .mergeWith(fetchRemainingImageUrlsFromPageList(pages))
+}
+
+fun OnlineSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
+    return Observable.from(pages)
+            .filter { it.imageUrl.isNullOrEmpty() }
+            .concatMap { getImageUrl(it) }
+}

+ 64 - 75
app/src/main/java/eu/kanade/tachiyomi/data/source/online/ParsedOnlineSource.kt

@@ -1,9 +1,9 @@
 package eu.kanade.tachiyomi.data.source.online
 
-import eu.kanade.tachiyomi.data.database.models.Chapter
-import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.source.model.MangasPage
 import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.data.source.model.SChapter
+import eu.kanade.tachiyomi.data.source.model.SManga
 import eu.kanade.tachiyomi.util.asJsoup
 import okhttp3.Response
 import org.jsoup.nodes.Document
@@ -12,26 +12,25 @@ import org.jsoup.nodes.Element
 /**
  * A simple implementation for sources from a website using Jsoup, an HTML parser.
  */
-abstract class ParsedOnlineSource() : OnlineSource() {
+abstract class ParsedOnlineSource : OnlineSource() {
 
     /**
-     * Parse the response from the site and fills [page].
+     * Parses the response from the site and returns a [MangasPage] object.
      *
      * @param response the response from the site.
-     * @param page the page object to be filled.
      */
-    override fun popularMangaParse(response: Response, page: MangasPage) {
+    override fun popularMangaParse(response: Response): MangasPage {
         val document = response.asJsoup()
-        for (element in document.select(popularMangaSelector())) {
-            Manga.create(id).apply {
-                popularMangaFromElement(element, this)
-                page.mangas.add(this)
-            }
-        }
 
-        popularMangaNextPageSelector()?.let { selector ->
-            page.nextPageUrl = document.select(selector).first()?.absUrl("href")
+        val mangas = document.select(popularMangaSelector()).map { element ->
+            popularMangaFromElement(element)
         }
+
+        val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
+            document.select(selector).first()
+        } != null
+
+        return MangasPage(mangas, hasNextPage)
     }
 
     /**
@@ -40,13 +39,12 @@ abstract class ParsedOnlineSource() : OnlineSource() {
     abstract protected fun popularMangaSelector(): String
 
     /**
-     * Fills [manga] with the given [element]. Most sites only show the title and the url, it's
-     * totally safe to fill only those two values.
+     * Returns a manga from the given [element]. Most sites only show the title and the url, it's
+     * totally fine to fill only those two values.
      *
      * @param element an element obtained from [popularMangaSelector].
-     * @param manga the manga to fill.
      */
-    abstract protected fun popularMangaFromElement(element: Element, manga: Manga)
+    abstract protected fun popularMangaFromElement(element: Element): SManga
 
     /**
      * Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
@@ -55,24 +53,22 @@ abstract class ParsedOnlineSource() : OnlineSource() {
     abstract protected fun popularMangaNextPageSelector(): String?
 
     /**
-     * Parse the response from the site and fills [page].
+     * Parses the response from the site and returns a [MangasPage] object.
      *
      * @param response the response from the site.
-     * @param page the page object to be filled.
-     * @param query the search query.
      */
-    override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) {
+    override fun searchMangaParse(response: Response): MangasPage {
         val document = response.asJsoup()
-        for (element in document.select(searchMangaSelector())) {
-            Manga.create(id).apply {
-                searchMangaFromElement(element, this)
-                page.mangas.add(this)
-            }
-        }
 
-        searchMangaNextPageSelector()?.let { selector ->
-            page.nextPageUrl = document.select(selector).first()?.absUrl("href")
+        val mangas = document.select(searchMangaSelector()).map { element ->
+            searchMangaFromElement(element)
         }
+
+        val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
+            document.select(selector).first()
+        } != null
+
+        return MangasPage(mangas, hasNextPage)
     }
 
     /**
@@ -81,13 +77,12 @@ abstract class ParsedOnlineSource() : OnlineSource() {
     abstract protected fun searchMangaSelector(): String
 
     /**
-     * Fills [manga] with the given [element]. Most sites only show the title and the url, it's
-     * totally safe to fill only those two values.
+     * Returns a manga from the given [element]. Most sites only show the title and the url, it's
+     * totally fine to fill only those two values.
      *
      * @param element an element obtained from [searchMangaSelector].
-     * @param manga the manga to fill.
      */
-    abstract protected fun searchMangaFromElement(element: Element, manga: Manga)
+    abstract protected fun searchMangaFromElement(element: Element): SManga
 
     /**
      * Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
@@ -96,70 +91,67 @@ abstract class ParsedOnlineSource() : OnlineSource() {
     abstract protected fun searchMangaNextPageSelector(): String?
 
     /**
-     * Parse the response from the site for latest updates and fills [page].
+     * Parses the response from the site and returns a [MangasPage] object.
+     *
+     * @param response the response from the site.
      */
-    override fun latestUpdatesParse(response: Response, page: MangasPage) {
+    override fun latestUpdatesParse(response: Response): MangasPage {
         val document = response.asJsoup()
-        for (element in document.select(latestUpdatesSelector())) {
-            Manga.create(id).apply {
-                latestUpdatesFromElement(element, this)
-                page.mangas.add(this)
-            }
-        }
 
-        latestUpdatesNextPageSelector()?.let { selector ->
-            page.nextPageUrl = document.select(selector).first()?.absUrl("href")
+        val mangas = document.select(latestUpdatesSelector()).map { element ->
+            latestUpdatesFromElement(element)
         }
+
+        val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
+            document.select(selector).first()
+        } != null
+
+        return MangasPage(mangas, hasNextPage)
     }
 
     /**
-     * Returns the Jsoup selector similar to [popularMangaSelector], but for latest updates.
+     * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
      */
     abstract protected fun latestUpdatesSelector(): String
 
     /**
-     * Fills [manga] with the given [element]. For latest updates.
+     * Returns a manga from the given [element]. Most sites only show the title and the url, it's
+     * totally fine to fill only those two values.
+     *
+     * @param element an element obtained from [latestUpdatesSelector].
      */
-    abstract protected fun latestUpdatesFromElement(element: Element, manga: Manga)
+    abstract protected fun latestUpdatesFromElement(element: Element): SManga
 
     /**
-     * Returns the Jsoup selector that returns the <a> tag, like [popularMangaNextPageSelector].
+     * Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
+     * there's no next page.
      */
     abstract protected fun latestUpdatesNextPageSelector(): String?
 
     /**
-     * Parse the response from the site and fills the details of [manga].
+     * Parses the response from the site and returns the details of a manga.
      *
      * @param response the response from the site.
-     * @param manga the manga to fill.
      */
-    override fun mangaDetailsParse(response: Response, manga: Manga) {
-        mangaDetailsParse(response.asJsoup(), manga)
+    override fun mangaDetailsParse(response: Response): SManga {
+        return mangaDetailsParse(response.asJsoup())
     }
 
     /**
-     * Fills the details of [manga] from the given [document].
+     * Returns the details of the manga from the given [document].
      *
      * @param document the parsed document.
-     * @param manga the manga to fill.
      */
-    abstract protected fun mangaDetailsParse(document: Document, manga: Manga)
+    abstract protected fun mangaDetailsParse(document: Document): SManga
 
     /**
-     * Parse the response from the site and fills the chapter list.
+     * Parses the response from the site and returns a list of chapters.
      *
      * @param response the response from the site.
-     * @param chapters the list of chapters to fill.
      */
-    override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
+    override fun chapterListParse(response: Response): List<SChapter> {
         val document = response.asJsoup()
-
-        for (element in document.select(chapterListSelector())) {
-            Chapter.create().apply {
-                chapterFromElement(element, this)
-                chapters.add(this)
-            }
-        }
+        return document.select(chapterListSelector()).map { chapterFromElement(it) }
     }
 
     /**
@@ -168,30 +160,27 @@ abstract class ParsedOnlineSource() : OnlineSource() {
     abstract protected fun chapterListSelector(): String
 
     /**
-     * Fills [chapter] with the given [element].
+     * Returns a chapter from the given element.
      *
      * @param element an element obtained from [chapterListSelector].
-     * @param chapter the chapter to fill.
      */
-    abstract protected fun chapterFromElement(element: Element, chapter: Chapter)
+    abstract protected fun chapterFromElement(element: Element): SChapter
 
     /**
-     * Parse the response from the site and fills the page list.
+     * Parses the response from the site and returns the page list.
      *
      * @param response the response from the site.
-     * @param pages the list of pages to fill.
      */
-    override fun pageListParse(response: Response, pages: MutableList<Page>) {
-        pageListParse(response.asJsoup(), pages)
+    override fun pageListParse(response: Response): List<Page> {
+        return pageListParse(response.asJsoup())
     }
 
     /**
-     * Fills [pages] from the given [document].
+     * Returns a page list from the given document.
      *
      * @param document the parsed document.
-     * @param pages the list of pages to fill.
      */
-    abstract protected fun pageListParse(document: Document, pages: MutableList<Page>)
+    abstract protected fun pageListParse(document: Document): List<Page>
 
     /**
      * Parse the response from the site and returns the absolute url to the source image.

+ 69 - 49
app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSource.kt

@@ -1,11 +1,8 @@
 package eu.kanade.tachiyomi.data.source.online
 
-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.model.MangasPage
-import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.data.source.model.*
 import eu.kanade.tachiyomi.util.asJsoup
 import eu.kanade.tachiyomi.util.attrOrText
 import okhttp3.Request
@@ -36,92 +33,108 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
     }
 
     override val id = map.id.let {
-        if (it is Int) it else (lang.toUpperCase().hashCode() + 31 * it.hashCode()) and 0x7fffffff
+        (it as? Int ?: (lang.toUpperCase().hashCode() + 31 * it.hashCode()) and 0x7fffffff).toLong()
     }
 
-    override fun popularMangaRequest(page: MangasPage): Request {
-        if (page.page == 1) {
-            page.url = popularMangaInitialUrl()
+    // Ugly, but needed after the changes
+    var popularNextPage: String? = null
+    var searchNextPage: String? = null
+    var latestNextPage: String? = null
+
+    override fun popularMangaRequest(page: Int): Request {
+        val url = if (page == 1) {
+            popularNextPage = null
+            map.popular.url
+        } else {
+            popularNextPage!!
         }
         return when (map.popular.method?.toLowerCase()) {
-            "post" -> POST(page.url, headers, map.popular.createForm())
-            else -> GET(page.url, headers)
+            "post" -> POST(url, headers, map.popular.createForm())
+            else -> GET(url, headers)
         }
     }
 
-    override fun popularMangaInitialUrl() = map.popular.url
-
-    override fun popularMangaParse(response: Response, page: MangasPage) {
+    override fun popularMangaParse(response: Response): MangasPage {
         val document = response.asJsoup()
-        for (element in document.select(map.popular.manga_css)) {
-            Manga.create(id).apply {
+
+        val mangas = document.select(map.popular.manga_css).map { element ->
+            SManga.create().apply {
                 title = element.text()
                 setUrlWithoutDomain(element.attr("href"))
-                page.mangas.add(this)
             }
         }
 
-        map.popular.next_url_css?.let { selector ->
-            page.nextPageUrl = document.select(selector).first()?.absUrl("href")
+        popularNextPage = map.popular.next_url_css?.let { selector ->
+             document.select(selector).first()?.absUrl("href")
         }
+
+        return MangasPage(mangas, popularNextPage != null)
     }
 
-    override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): Request {
-        if (page.page == 1) {
-            page.url = searchMangaInitialUrl(query, filters)
+    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+        val url = if (page == 1) {
+            searchNextPage = null
+            map.search.url.replace("\$query", query)
+        } else {
+            searchNextPage!!
         }
         return when (map.search.method?.toLowerCase()) {
-            "post" -> POST(page.url, headers, map.search.createForm())
-            else -> GET(page.url, headers)
+            "post" -> POST(url, headers, map.search.createForm())
+            else -> GET(url, headers)
         }
     }
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) = map.search.url.replace("\$query", query)
-
-    override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) {
+    override fun searchMangaParse(response: Response): MangasPage {
         val document = response.asJsoup()
-        for (element in document.select(map.search.manga_css)) {
-            Manga.create(id).apply {
+
+        val mangas = document.select(map.search.manga_css).map { element ->
+            SManga.create().apply {
                 title = element.text()
                 setUrlWithoutDomain(element.attr("href"))
-                page.mangas.add(this)
             }
         }
 
-        map.search.next_url_css?.let { selector ->
-            page.nextPageUrl = document.select(selector).first()?.absUrl("href")
+        searchNextPage = map.search.next_url_css?.let { selector ->
+            document.select(selector).first()?.absUrl("href")
         }
+
+        return MangasPage(mangas, searchNextPage != null)
     }
 
-    override fun latestUpdatesRequest(page: MangasPage): Request {
-        if (page.page == 1) {
-            page.url = latestUpdatesInitialUrl()
+    override fun latestUpdatesRequest(page: Int): Request {
+        val url = if (page == 1) {
+            latestNextPage = null
+            map.latestupdates!!.url
+        } else {
+            latestNextPage!!
         }
         return when (map.latestupdates!!.method?.toLowerCase()) {
-            "post" -> POST(page.url, headers, map.latestupdates.createForm())
-            else -> GET(page.url, headers)
+            "post" -> POST(url, headers, map.latestupdates.createForm())
+            else -> GET(url, headers)
         }
     }
 
-    override fun latestUpdatesInitialUrl() = map.latestupdates!!.url
-
-    override fun latestUpdatesParse(response: Response, page: MangasPage) {
+    override fun latestUpdatesParse(response: Response): MangasPage {
         val document = response.asJsoup()
-        for (element in document.select(map.latestupdates!!.manga_css)) {
-            Manga.create(id).apply {
+
+        val mangas = document.select(map.latestupdates!!.manga_css).map { element ->
+            SManga.create().apply {
                 title = element.text()
                 setUrlWithoutDomain(element.attr("href"))
-                page.mangas.add(this)
             }
         }
 
-        map.latestupdates.next_url_css?.let { selector ->
-            page.nextPageUrl = document.select(selector).first()?.absUrl("href")
+        popularNextPage = map.latestupdates.next_url_css?.let { selector ->
+            document.select(selector).first()?.absUrl("href")
         }
+
+        return MangasPage(mangas, popularNextPage != null)
     }
 
-    override fun mangaDetailsParse(response: Response, manga: Manga) {
+    override fun mangaDetailsParse(response: Response): SManga {
         val document = response.asJsoup()
+
+        val manga = SManga.create()
         with(map.manga) {
             val pool = parts.get(document)
 
@@ -130,18 +143,21 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
             manga.description = summary?.process(document, pool)
             manga.thumbnail_url = cover?.process(document, pool)
             manga.genre = genres?.process(document, pool)
-            manga.status = status?.getStatus(document, pool) ?: Manga.UNKNOWN
+            manga.status = status?.getStatus(document, pool) ?: SManga.UNKNOWN
         }
+        return manga
     }
 
-    override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
+    override fun chapterListParse(response: Response): List<SChapter> {
         val document = response.asJsoup()
+
+        val chapters = mutableListOf<SChapter>()
         with(map.chapters) {
             val pool = emptyMap<String, Element>()
             val dateFormat = SimpleDateFormat(date?.format, Locale.ENGLISH)
 
             for (element in document.select(chapter_css)) {
-                val chapter = Chapter.create()
+                val chapter = SChapter.create()
                 element.select(title).first().let {
                     chapter.name = it.text()
                     chapter.setUrlWithoutDomain(it.attr("href"))
@@ -151,12 +167,15 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
                 chapters.add(chapter)
             }
         }
+        return chapters
     }
 
-    override fun pageListParse(response: Response, pages: MutableList<Page>) {
+    override fun pageListParse(response: Response): List<Page> {
         val body = response.body().string()
         val url = response.request().url().toString()
 
+        val pages = mutableListOf<Page>()
+
         // TODO lazy initialization in Kotlin 1.1
         val document = Jsoup.parse(body, url)
 
@@ -194,6 +213,7 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
                 page.imageUrl = url
             }
         }
+        return pages
     }
 
     override fun imageUrlParse(response: Response): String {

+ 5 - 5
app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSourceMappings.kt

@@ -2,7 +2,7 @@
 
 package eu.kanade.tachiyomi.data.source.online
 
-import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.source.model.SManga
 import okhttp3.FormBody
 import okhttp3.RequestBody
 import org.jsoup.nodes.Document
@@ -164,15 +164,15 @@ class StatusNode(private val map: Map<String, Any?>) : SelectableNode(map) {
     fun getStatus(document: Element, cache: Map<String, Element>): Int {
         val text = process(document, cache)
         complete?.let {
-            if (text.contains(it)) return Manga.COMPLETED
+            if (text.contains(it)) return SManga.COMPLETED
         }
         ongoing?.let {
-            if (text.contains(it)) return Manga.ONGOING
+            if (text.contains(it)) return SManga.ONGOING
         }
         licensed?.let {
-            if (text.contains(it)) return Manga.LICENSED
+            if (text.contains(it)) return SManga.LICENSED
         }
-        return Manga.UNKNOWN
+        return SManga.UNKNOWN
     }
 }
 

+ 42 - 87
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.kt

@@ -1,13 +1,10 @@
 package eu.kanade.tachiyomi.data.source.online.english
 
 import android.text.Html
-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.network.asObservable
-import eu.kanade.tachiyomi.data.source.model.MangasPage
-import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.data.source.model.*
 import eu.kanade.tachiyomi.data.source.online.LoginSource
 import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
 import eu.kanade.tachiyomi.util.asJsoup
@@ -25,7 +22,9 @@ import java.text.SimpleDateFormat
 import java.util.*
 import java.util.regex.Pattern
 
-class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
+class Batoto : ParsedOnlineSource(), LoginSource {
+
+    override val id: Long = 1
 
     override val name = "Batoto"
 
@@ -56,70 +55,46 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
             .add("Referer", "http://bato.to/reader")
             .build()
 
-    override fun popularMangaInitialUrl() = "$baseUrl/search_ajax?order_cond=views&order=desc&p=1"
-
-    override fun latestUpdatesInitialUrl() = "$baseUrl/search_ajax?order_cond=update&order=desc&p=1"
-
-    override fun popularMangaParse(response: Response, page: MangasPage) {
-        val document = response.asJsoup()
-        for (element in document.select(popularMangaSelector())) {
-            Manga.create(id).apply {
-                popularMangaFromElement(element, this)
-                page.mangas.add(this)
-            }
-        }
-
-        page.nextPageUrl = document.select(popularMangaNextPageSelector()).first()?.let {
-            "$baseUrl/search_ajax?order_cond=views&order=desc&p=${page.page + 1}"
-        }
+    override fun popularMangaRequest(page: Int): Request {
+        return GET("$baseUrl/search_ajax?order_cond=views&order=desc&p=$page", headers)
     }
 
-    override fun latestUpdatesParse(response: Response, page: MangasPage) {
-        val document = response.asJsoup()
-        for (element in document.select(latestUpdatesSelector())) {
-            Manga.create(id).apply {
-                latestUpdatesFromElement(element, this)
-                page.mangas.add(this)
-            }
-        }
-
-        page.nextPageUrl = document.select(latestUpdatesNextPageSelector()).first()?.let {
-            "$baseUrl/search_ajax?order_cond=update&order=desc&p=${page.page + 1}"
-        }
+    override fun latestUpdatesRequest(page: Int): Request {
+        return GET("$baseUrl/search_ajax?order_cond=update&order=desc&p=$page", headers)
     }
 
     override fun popularMangaSelector() = "tr:has(a)"
 
     override fun latestUpdatesSelector() = "tr:has(a)"
 
-    override fun popularMangaFromElement(element: Element, manga: Manga) {
+    override fun popularMangaFromElement(element: Element): SManga {
+        val manga = SManga.create()
         element.select("a[href^=http://bato.to]").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.text().trim()
         }
+        return manga
     }
 
-    override fun latestUpdatesFromElement(element: Element, manga: Manga) {
-        popularMangaFromElement(element, manga)
+    override fun latestUpdatesFromElement(element: Element): SManga {
+        return popularMangaFromElement(element)
     }
 
     override fun popularMangaNextPageSelector() = "#show_more_row"
 
     override fun latestUpdatesNextPageSelector() = "#show_more_row"
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) = searchMangaUrl(query, filters, 1)
-
-    private fun searchMangaUrl(query: String, filterStates: List<Filter<*>>, page: Int): String {
+    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
         val url = HttpUrl.parse("$baseUrl/search_ajax").newBuilder()
         if (!query.isEmpty()) url.addQueryParameter("name", query).addQueryParameter("name_cond", "c")
         var genres = ""
-        for (filter in if (filterStates.isEmpty()) filters else filterStates) {
+        filters.forEach { filter ->
             when (filter) {
-                is Status -> if (filter.state != Filter.TriState.STATE_IGNORE) {
-                    url.addQueryParameter("completed", if (filter.state == Filter.TriState.STATE_EXCLUDE) "i" else "c")
+                is Status -> if (!filter.isIgnored()) {
+                    url.addQueryParameter("completed", if (filter.isExcluded()) "i" else "c")
                 }
-                is Genre -> if (filter.state != Filter.TriState.STATE_IGNORE) {
-                    genres += (if (filter.state == Filter.TriState.STATE_EXCLUDE) ";e" else ";i") + filter.id
+                is Genre -> if (!filter.isIgnored()) {
+                    genres += (if (filter.isExcluded()) ";e" else ";i") + filter.id
                 }
                 is TextField -> {
                     if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
@@ -136,89 +111,67 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
         }
         if (!genres.isEmpty()) url.addQueryParameter("genres", genres)
         url.addQueryParameter("p", page.toString())
-        return url.toString()
-    }
-
-    override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): Request {
-        if (page.page == 1) {
-            page.url = searchMangaInitialUrl(query, filters)
-        }
-        return GET(page.url, headers)
-    }
-
-    override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) {
-        val document = response.asJsoup()
-        for (element in document.select(searchMangaSelector())) {
-            Manga.create(id).apply {
-                searchMangaFromElement(element, this)
-                page.mangas.add(this)
-            }
-        }
-
-        page.nextPageUrl = document.select(searchMangaNextPageSelector()).first()?.let {
-            searchMangaUrl(query, filters, page.page + 1)
-        }
+        return GET(url.toString(), headers)
     }
 
     override fun searchMangaSelector() = popularMangaSelector()
 
-    override fun searchMangaFromElement(element: Element, manga: Manga) {
-        popularMangaFromElement(element, manga)
+    override fun searchMangaFromElement(element: Element): SManga {
+        return popularMangaFromElement(element)
     }
 
     override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
 
-    override fun mangaDetailsRequest(manga: Manga): Request {
+    override fun mangaDetailsRequest(manga: SManga): Request {
         val mangaId = manga.url.substringAfterLast("r")
         return GET("$baseUrl/comic_pop?id=$mangaId", headers)
     }
 
-    override fun mangaDetailsParse(document: Document, manga: Manga) {
+    override fun mangaDetailsParse(document: Document): SManga {
         val tbody = document.select("tbody").first()
         val artistElement = tbody.select("tr:contains(Author/Artist:)").first()
 
+        val manga = SManga.create()
         manga.author = artistElement.selectText("td:eq(1)")
         manga.artist = artistElement.selectText("td:eq(2)") ?: manga.author
         manga.description = tbody.selectText("tr:contains(Description:) > td:eq(1)")
         manga.thumbnail_url = document.select("img[src^=http://img.bato.to/forums/uploads/]").first()?.attr("src")
         manga.status = parseStatus(document.selectText("tr:contains(Status:) > td:eq(1)"))
         manga.genre = tbody.select("tr:contains(Genres:) img").map { it.attr("alt") }.joinToString(", ")
+        return manga
     }
 
     private fun parseStatus(status: String?) = when (status) {
-        "Ongoing" -> Manga.ONGOING
-        "Complete" -> Manga.COMPLETED
-        else -> Manga.UNKNOWN
+        "Ongoing" -> SManga.ONGOING
+        "Complete" -> SManga.COMPLETED
+        else -> SManga.UNKNOWN
     }
 
-    override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
+    override fun chapterListParse(response: Response): List<SChapter> {
         val body = response.body().string()
         val matcher = staffNotice.matcher(body)
         if (matcher.find()) {
+            @Suppress("DEPRECATION")
             val notice = Html.fromHtml(matcher.group(1)).toString().trim()
             throw Exception(notice)
         }
 
         val document = response.asJsoup(body)
-
-        for (element in document.select(chapterListSelector())) {
-            Chapter.create().apply {
-                chapterFromElement(element, this)
-                chapters.add(this)
-            }
-        }
+        return document.select(chapterListSelector()).map { chapterFromElement(it) }
     }
 
     override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
 
-    override fun chapterFromElement(element: Element, chapter: Chapter) {
+    override fun chapterFromElement(element: Element): SChapter {
         val urlElement = element.select("a[href^=http://bato.to/reader").first()
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.name = urlElement.text()
         chapter.date_upload = element.select("td").getOrNull(4)?.let {
             parseDateFromElement(it)
         } ?: 0
+        return chapter
     }
 
     private fun parseDateFromElement(dateElement: Element): Long {
@@ -246,12 +199,13 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
         return date.time
     }
 
-    override fun pageListRequest(chapter: Chapter): Request {
+    override fun pageListRequest(chapter: SChapter): Request {
         val id = chapter.url.substringAfterLast("#")
         return GET("$baseUrl/areader?id=$id&p=1", pageHeaders)
     }
 
-    override fun pageListParse(document: Document, pages: MutableList<Page>) {
+    override fun pageListParse(document: Document): List<Page> {
+        val pages = mutableListOf<Page>()
         val selectElement = document.select("#page_select").first()
         if (selectElement != null) {
             for ((i, element) in selectElement.select("option").withIndex()) {
@@ -264,6 +218,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
                 pages.add(Page(i, "", element.attr("src")))
             }
         }
+        return pages
     }
 
     override fun imageUrlRequest(page: Page): Request {
@@ -308,7 +263,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
         return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
     }
 
-    override fun fetchChapterList(manga: Manga): Observable<List<Chapter>> {
+    override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
         if (!isLogged()) {
             val username = preferences.sourceUsername(this)
             val password = preferences.sourcePassword(this)
@@ -328,7 +283,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
         override fun toString(): String = name
     }
 
-    private class Status() : Filter.TriState("Completed")
+    private class Status : Filter.TriState("Completed")
     private class Genre(name: String, val id: Int) : Filter.TriState(name)
     private class TextField(name: String, val key: String) : Filter.Text(name)
     private class ListField(name: String, val key: String, values: Array<ListValue>, state: Int = 0) : Filter.List<ListValue>(name, values, state)
@@ -338,7 +293,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
     //     const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Genre("${el.textContent.trim()}", ${id})`
     // }).join(',\n')
     // on https://bato.to/search
-    override fun getFilterList(): List<Filter<*>> = listOf(
+    override fun getFilterList() = FilterList(
             TextField("Author", "artist_name"),
             ListField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Manga (Jp)", "jp"), ListValue("Manhwa (Kr)", "kr"), ListValue("Manhua (Cn)", "cn"), ListValue("Artbook", "ar"), ListValue("Other", "ot"))),
             Status(),

+ 84 - 78
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.kt

@@ -1,11 +1,8 @@
 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.GET
 import eu.kanade.tachiyomi.data.network.POST
-import eu.kanade.tachiyomi.data.source.model.MangasPage
-import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.data.source.model.*
 import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
 import okhttp3.FormBody
 import okhttp3.OkHttpClient
@@ -16,7 +13,9 @@ import org.jsoup.nodes.Element
 import java.text.SimpleDateFormat
 import java.util.regex.Pattern
 
-class Kissmanga(override val id: Int) : ParsedOnlineSource() {
+class Kissmanga : ParsedOnlineSource() {
+
+    override val id: Long = 4
 
     override val name = "Kissmanga"
 
@@ -28,38 +27,40 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
 
     override val client: OkHttpClient = network.cloudflareClient
 
-    override fun popularMangaInitialUrl() = "$baseUrl/MangaList/MostPopular"
-
-    override fun latestUpdatesInitialUrl() = "http://kissmanga.com/MangaList/LatestUpdate"
-
     override fun popularMangaSelector() = "table.listing tr:gt(1)"
 
     override fun latestUpdatesSelector() = "table.listing tr:gt(1)"
 
-    override fun popularMangaFromElement(element: Element, manga: Manga) {
+    override fun popularMangaRequest(page: Int): Request {
+        return GET("$baseUrl/MangaList/MostPopular?page=$page", headers)
+    }
+
+    override fun latestUpdatesRequest(page: Int): Request {
+        return GET("http://kissmanga.com/MangaList/LatestUpdate?page=$page", headers)
+    }
+
+    override fun popularMangaFromElement(element: Element): SManga {
+        val manga = SManga.create()
         element.select("td a:eq(0)").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.text()
         }
+        return manga
     }
 
-    override fun latestUpdatesFromElement(element: Element, manga: Manga) {
-        popularMangaFromElement(element, manga)
+    override fun latestUpdatesFromElement(element: Element): SManga {
+        return popularMangaFromElement(element)
     }
 
     override fun popularMangaNextPageSelector() = "li > a:contains(› Next)"
 
     override fun latestUpdatesNextPageSelector(): String = "ul.pager > li > a:contains(Next)"
 
-    override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): Request {
-        if (page.page == 1) {
-            page.url = searchMangaInitialUrl(query, filters)
-        }
-
+    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
         val form = FormBody.Builder().apply {
             add("mangaName", query)
 
-            for (filter in if (filters.isEmpty()) [email protected] else filters) {
+            for (filter in if (filters.isEmpty()) getFilterList() else filters) {
                 when (filter) {
                     is Author -> add("authorArtist", filter.state)
                     is Status -> add("status", arrayOf("", "Completed", "Ongoing")[filter.state])
@@ -67,50 +68,53 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
                 }
             }
         }
-        return POST(page.url, headers, form.build())
+        return POST("$baseUrl/AdvanceSearch", headers, form.build())
     }
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) = "$baseUrl/AdvanceSearch"
-
     override fun searchMangaSelector() = popularMangaSelector()
 
-    override fun searchMangaFromElement(element: Element, manga: Manga) {
-        popularMangaFromElement(element, manga)
+    override fun searchMangaFromElement(element: Element): SManga {
+        return popularMangaFromElement(element)
     }
 
     override fun searchMangaNextPageSelector() = null
 
-    override fun mangaDetailsParse(document: Document, manga: Manga) {
+    override fun mangaDetailsParse(document: Document): SManga {
         val infoElement = document.select("div.barContent").first()
 
+        val manga = SManga.create()
         manga.author = infoElement.select("p:has(span:contains(Author:)) > a").first()?.text()
         manga.genre = infoElement.select("p:has(span:contains(Genres:)) > *:gt(0)").text()
         manga.description = infoElement.select("p:has(span:contains(Summary:)) ~ p").text()
         manga.status = infoElement.select("p:has(span:contains(Status:))").first()?.text().orEmpty().let { parseStatus(it) }
         manga.thumbnail_url = document.select(".rightBox:eq(0) img").first()?.attr("src")
+        return manga
     }
 
     fun parseStatus(status: String) = when {
-        status.contains("Ongoing") -> Manga.ONGOING
-        status.contains("Completed") -> Manga.COMPLETED
-        else -> Manga.UNKNOWN
+        status.contains("Ongoing") -> SManga.ONGOING
+        status.contains("Completed") -> SManga.COMPLETED
+        else -> SManga.UNKNOWN
     }
 
     override fun chapterListSelector() = "table.listing tr:gt(1)"
 
-    override fun chapterFromElement(element: Element, chapter: Chapter) {
+    override fun chapterFromElement(element: Element): SChapter {
         val urlElement = element.select("a").first()
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.name = urlElement.text()
         chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
             SimpleDateFormat("MM/dd/yyyy").parse(it).time
         } ?: 0
+        return chapter
     }
 
-    override fun pageListRequest(chapter: Chapter) = POST(baseUrl + chapter.url, headers)
+    override fun pageListRequest(chapter: SChapter) = POST(baseUrl + chapter.url, headers)
 
-    override fun pageListParse(response: Response, pages: MutableList<Page>) {
+    override fun pageListParse(response: Response): List<Page> {
+        val pages = mutableListOf<Page>()
         //language=RegExp
         val p = Pattern.compile("""lstImages.push\("(.+?)"""")
         val m = p.matcher(response.body().string())
@@ -119,10 +123,11 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
         while (m.find()) {
             pages.add(Page(i++, "", m.group(1)))
         }
+        return pages
     }
 
-    // Not used
-    override fun pageListParse(document: Document, pages: MutableList<Page>) {
+    override fun pageListParse(document: Document): List<Page> {
+        throw Exception("Not used")
     }
 
     override fun imageUrlRequest(page: Page) = GET(page.url)
@@ -131,57 +136,58 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
 
     private class Status() : Filter.TriState("Completed")
     private class Author() : Filter.Text("Author")
-    private class Genre(name: String, val id: Int) : Filter.TriState(name)
+    private class Genre(name: String) : Filter.TriState(name)
 
     // $("select[name=\"genres\"]").map((i,el) => `Genre("${$(el).next().text().trim()}", ${i})`).get().join(',\n')
     // on http://kissmanga.com/AdvanceSearch
-    override fun getFilterList(): List<Filter<*>> = listOf(
+    override fun getFilterList() = FilterList(
             Author(),
             Status(),
             Filter.Header("Genres"),
-            Genre("Action", 0),
-            Genre("Adult", 1),
-            Genre("Adventure", 2),
-            Genre("Comedy", 3),
-            Genre("Comic", 4),
-            Genre("Cooking", 5),
-            Genre("Doujinshi", 6),
-            Genre("Drama", 7),
-            Genre("Ecchi", 8),
-            Genre("Fantasy", 9),
-            Genre("Gender Bender", 10),
-            Genre("Harem", 11),
-            Genre("Historical", 12),
-            Genre("Horror", 13),
-            Genre("Josei", 14),
-            Genre("Lolicon", 15),
-            Genre("Manga", 16),
-            Genre("Manhua", 17),
-            Genre("Manhwa", 18),
-            Genre("Martial Arts", 19),
-            Genre("Mature", 20),
-            Genre("Mecha", 21),
-            Genre("Medical", 22),
-            Genre("Music", 23),
-            Genre("Mystery", 24),
-            Genre("One shot", 25),
-            Genre("Psychological", 26),
-            Genre("Romance", 27),
-            Genre("School Life", 28),
-            Genre("Sci-fi", 29),
-            Genre("Seinen", 30),
-            Genre("Shotacon", 31),
-            Genre("Shoujo", 32),
-            Genre("Shoujo Ai", 33),
-            Genre("Shounen", 34),
-            Genre("Shounen Ai", 35),
-            Genre("Slice of Life", 36),
-            Genre("Smut", 37),
-            Genre("Sports", 38),
-            Genre("Supernatural", 39),
-            Genre("Tragedy", 40),
-            Genre("Webtoon", 41),
-            Genre("Yaoi", 42),
-            Genre("Yuri", 43)
+            Genre("4-Koma"),
+            Genre("Action"),
+            Genre("Adult"),
+            Genre("Adventure"),
+            Genre("Comedy"),
+            Genre("Comic"),
+            Genre("Cooking"),
+            Genre("Doujinshi"),
+            Genre("Drama"),
+            Genre("Ecchi"),
+            Genre("Fantasy"),
+            Genre("Gender Bender"),
+            Genre("Harem"),
+            Genre("Historical"),
+            Genre("Horror"),
+            Genre("Josei"),
+            Genre("Lolicon"),
+            Genre("Manga"),
+            Genre("Manhua"),
+            Genre("Manhwa"),
+            Genre("Martial Arts"),
+            Genre("Mature"),
+            Genre("Mecha"),
+            Genre("Medical"),
+            Genre("Music"),
+            Genre("Mystery"),
+            Genre("One shot"),
+            Genre("Psychological"),
+            Genre("Romance"),
+            Genre("School Life"),
+            Genre("Sci-fi"),
+            Genre("Seinen"),
+            Genre("Shotacon"),
+            Genre("Shoujo"),
+            Genre("Shoujo Ai"),
+            Genre("Shounen"),
+            Genre("Shounen Ai"),
+            Genre("Slice of Life"),
+            Genre("Smut"),
+            Genre("Sports"),
+            Genre("Supernatural"),
+            Genre("Tragedy"),
+            Genre("Webtoon"),
+            Genre("Yaoi"),
+            Genre("Yuri")
     )
 }

+ 42 - 30
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangafox.kt

@@ -1,19 +1,19 @@
 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.model.Page
+import eu.kanade.tachiyomi.data.network.GET
+import eu.kanade.tachiyomi.data.source.model.*
 import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
-import eu.kanade.tachiyomi.util.asJsoup
 import okhttp3.HttpUrl
-import okhttp3.Response
+import okhttp3.Request
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
 import java.text.ParseException
 import java.text.SimpleDateFormat
 import java.util.*
 
-class Mangafox(override val id: Int) : ParsedOnlineSource() {
+class Mangafox : ParsedOnlineSource() {
+
+    override val id: Long = 3
 
     override val name = "Mangafox"
 
@@ -23,32 +23,40 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
 
     override val supportsLatest = true
 
-    override fun popularMangaInitialUrl() = "$baseUrl/directory/"
-
-    override fun latestUpdatesInitialUrl() = "$baseUrl/directory/?latest"
-
     override fun popularMangaSelector() = "div#mangalist > ul.list > li"
+    
+    override fun popularMangaRequest(page: Int): Request {
+        val pageStr = if (page != 1) "$page.htm" else ""
+        return GET("$baseUrl/directory/$pageStr", headers)
+    }
 
     override fun latestUpdatesSelector() = "div#mangalist > ul.list > li"
 
-    override fun popularMangaFromElement(element: Element, manga: Manga) {
+    override fun latestUpdatesRequest(page: Int): Request {
+        val pageStr = if (page != 1) "$page.htm" else ""
+        return GET("$baseUrl/directory/$pageStr?latest")
+    }
+
+    override fun popularMangaFromElement(element: Element): SManga {
+        val manga = SManga.create()
         element.select("a.title").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.text()
         }
+        return manga
     }
 
-    override fun latestUpdatesFromElement(element: Element, manga: Manga) {
-        popularMangaFromElement(element, manga)
+    override fun latestUpdatesFromElement(element: Element): SManga {
+        return popularMangaFromElement(element)
     }
 
     override fun popularMangaNextPageSelector() = "a:has(span.next)"
 
     override fun latestUpdatesNextPageSelector() = "a:has(span.next)"
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String {
+    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
         val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
-        for (filter in if (filters.isEmpty()) [email protected] else filters) {
+        (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
             when (filter) {
                 is Genre -> url.addQueryParameter(filter.id, filter.state.toString())
                 is TextField -> url.addQueryParameter(filter.key, filter.state)
@@ -56,47 +64,54 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
                 is Order -> url.addQueryParameter("order", if (filter.state) "az" else "za")
             }
         }
-        return url.toString()
+        url.addQueryParameter("page", page.toString())
+        return GET(url.toString(), headers)
     }
 
     override fun searchMangaSelector() = "div#mangalist > ul.list > li"
 
-    override fun searchMangaFromElement(element: Element, manga: Manga) {
+    override fun searchMangaFromElement(element: Element): SManga {
+        val manga = SManga.create()
         element.select("a.title").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.text()
         }
+        return manga
     }
 
     override fun searchMangaNextPageSelector() = "a:has(span.next)"
 
-    override fun mangaDetailsParse(document: Document, manga: Manga) {
+    override fun mangaDetailsParse(document: Document): SManga {
         val infoElement = document.select("div#title").first()
         val rowElement = infoElement.select("table > tbody > tr:eq(1)").first()
         val sideInfoElement = document.select("#series_info").first()
 
+        val manga = SManga.create()
         manga.author = rowElement.select("td:eq(1)").first()?.text()
         manga.artist = rowElement.select("td:eq(2)").first()?.text()
         manga.genre = rowElement.select("td:eq(3)").first()?.text()
         manga.description = infoElement.select("p.summary").first()?.text()
         manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) }
         manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src")
+        return manga
     }
 
     private fun parseStatus(status: String) = when {
-        status.contains("Ongoing") -> Manga.ONGOING
-        status.contains("Completed") -> Manga.COMPLETED
-        else -> Manga.UNKNOWN
+        status.contains("Ongoing") -> SManga.ONGOING
+        status.contains("Completed") -> SManga.COMPLETED
+        else -> SManga.UNKNOWN
     }
 
     override fun chapterListSelector() = "div#chapters li div"
 
-    override fun chapterFromElement(element: Element, chapter: Chapter) {
+    override fun chapterFromElement(element: Element): SChapter {
         val urlElement = element.select("a.tips").first()
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.name = urlElement.text()
         chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
+        return chapter
     }
 
     private fun parseChapterDate(date: String): Long {
@@ -124,17 +139,14 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
         }
     }
 
-    override fun pageListParse(response: Response, pages: MutableList<Page>) {
-        val document = response.asJsoup()
+    override fun pageListParse(document: Document): List<Page> {
+        val url = document.baseUri().substringBeforeLast('/')
 
-        val url = response.request().url().toString().substringBeforeLast('/')
+        val pages = mutableListOf<Page>()
         document.select("select.m").first()?.select("option:not([value=0])")?.forEach {
             pages.add(Page(pages.size, "$url/${it.attr("value")}.html"))
         }
-    }
-
-    // Not used, overrides parent.
-    override fun pageListParse(document: Document, pages: MutableList<Page>) {
+        return pages
     }
 
     override fun imageUrlParse(document: Document): String {
@@ -157,7 +169,7 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
 
     // $('select.genres').map((i,el)=>`Genre("${$(el).next().text().trim()}", "${$(el).attr('name')}")`).get().join(',\n')
     // on http://mangafox.me/search.php
-    override fun getFilterList(): List<Filter<*>> = listOf(
+    override fun getFilterList() = FilterList(
             TextField("Author", "author"),
             TextField("Artist", "artist"),
             ListField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Japanese Manga", "1"), ListValue("Korean Manhwa", "2"), ListValue("Chinese Manhua", "3"))),

+ 40 - 26
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.kt

@@ -1,17 +1,19 @@
 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.model.Page
+import eu.kanade.tachiyomi.data.network.GET
+import eu.kanade.tachiyomi.data.source.model.*
 import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
 import okhttp3.HttpUrl
+import okhttp3.Request
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
 import java.text.ParseException
 import java.text.SimpleDateFormat
 import java.util.*
 
-class Mangahere(override val id: Int) : ParsedOnlineSource() {
+class Mangahere : ParsedOnlineSource() {
+
+    override val id: Long = 2
 
     override val name = "Mangahere"
 
@@ -21,36 +23,42 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
 
     override val supportsLatest = true
 
-    override fun popularMangaInitialUrl() = "$baseUrl/directory/?views.za"
-
-    override fun latestUpdatesInitialUrl() = "$baseUrl/directory/?last_chapter_time.za"
-
     override fun popularMangaSelector() = "div.directory_list > ul > li"
 
     override fun latestUpdatesSelector() = "div.directory_list > ul > li"
 
-    private fun mangaFromElement(query: String, element: Element, manga: Manga) {
+    override fun popularMangaRequest(page: Int): Request {
+        return GET("$baseUrl/directory/$page.htm?views.za", headers)
+    }
+
+    override fun latestUpdatesRequest(page: Int): Request {
+        return GET("$baseUrl/directory/$page.htm?last_chapter_time.za", headers)
+    }
+
+    private fun mangaFromElement(query: String, element: Element): SManga {
+        val manga = SManga.create()
         element.select(query).first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = if (it.hasAttr("title")) it.attr("title") else if (it.hasAttr("rel")) it.attr("rel") else it.text()
         }
+        return manga
     }
 
-    override fun popularMangaFromElement(element: Element, manga: Manga) {
-        mangaFromElement("div.title > a", element, manga)
+    override fun popularMangaFromElement(element: Element): SManga {
+        return mangaFromElement("div.title > a", element)
     }
 
-    override fun latestUpdatesFromElement(element: Element, manga: Manga) {
-        popularMangaFromElement(element, manga)
+    override fun latestUpdatesFromElement(element: Element): SManga {
+        return popularMangaFromElement(element)
     }
 
     override fun popularMangaNextPageSelector() = "div.next-page > a.next"
 
     override fun latestUpdatesNextPageSelector() = "div.next-page > a.next"
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String {
+    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
         val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
-        for (filter in if (filters.isEmpty()) [email protected] else filters) {
+        (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
             when (filter) {
                 is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
                 is Genre -> url.addQueryParameter(filter.id, filter.state.toString())
@@ -59,39 +67,41 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
                 is Order -> url.addQueryParameter("order", if (filter.state) "az" else "za")
             }
         }
-        return url.toString()
+        url.addQueryParameter("page", page.toString())
+        return GET(url.toString(), headers)
     }
 
-
     override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
 
-    override fun searchMangaFromElement(element: Element, manga: Manga) {
-        mangaFromElement("a.manga_info", element, manga)
+    override fun searchMangaFromElement(element: Element): SManga {
+        return mangaFromElement("a.manga_info", element)
     }
 
     override fun searchMangaNextPageSelector() = "div.next-page > a.next"
 
-    override fun mangaDetailsParse(document: Document, manga: Manga) {
+    override fun mangaDetailsParse(document: Document): SManga {
         val detailElement = document.select(".manga_detail_top").first()
         val infoElement = detailElement.select(".detail_topText").first()
 
+        val manga = SManga.create()
         manga.author = infoElement.select("a[href^=http://www.mangahere.co/author/]").first()?.text()
         manga.artist = infoElement.select("a[href^=http://www.mangahere.co/artist/]").first()?.text()
         manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):")
         manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less")
         manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) }
         manga.thumbnail_url = detailElement.select("img.img").first()?.attr("src")
+        return manga
     }
 
     private fun parseStatus(status: String) = when {
-        status.contains("Ongoing") -> Manga.ONGOING
-        status.contains("Completed") -> Manga.COMPLETED
-        else -> Manga.UNKNOWN
+        status.contains("Ongoing") -> SManga.ONGOING
+        status.contains("Completed") -> SManga.COMPLETED
+        else -> SManga.UNKNOWN
     }
 
     override fun chapterListSelector() = ".detail_list > ul:not([class]) > li"
 
-    override fun chapterFromElement(element: Element, chapter: Chapter) {
+    override fun chapterFromElement(element: Element): SChapter {
         val parentEl = element.select("span.left").first()
 
         val urlElement = parentEl.select("a").first()
@@ -106,9 +116,11 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
             title = " - " + title
         }
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.name = urlElement.text() + volume + title
         chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0
+        return chapter
     }
 
     private fun parseChapterDate(date: String): Long {
@@ -136,11 +148,13 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
         }
     }
 
-    override fun pageListParse(document: Document, pages: MutableList<Page>) {
+    override fun pageListParse(document: Document): List<Page> {
+        val pages = mutableListOf<Page>()
         document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
             pages.add(Page(pages.size, it.attr("value")))
         }
         pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
+        return pages
     }
 
     override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
@@ -157,7 +171,7 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
 
     // [...document.querySelectorAll("select[id^='genres'")].map((el,i) => `Genre("${el.nextSibling.nextSibling.textContent.trim()}", "${el.getAttribute('name')}")`).join(',\n')
     // http://www.mangahere.co/advsearch.htm
-    override fun getFilterList(): List<Filter<*>> = listOf(
+    override fun getFilterList() = FilterList(
             TextField("Author", "author"),
             TextField("Artist", "artist"),
             ListField("Type", "direction", arrayOf(ListValue("Any", ""), ListValue("Japanese Manga (read from right to left)", "rl"), ListValue("Korean Manhwa (read from left to right)", "lr"))),

+ 44 - 93
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangasee.kt

@@ -1,22 +1,19 @@
 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.model.MangasPage
-import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.data.source.model.*
 import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
-import eu.kanade.tachiyomi.util.asJsoup
 import okhttp3.FormBody
 import okhttp3.HttpUrl
 import okhttp3.Request
-import okhttp3.Response
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
 import java.text.SimpleDateFormat
 import java.util.regex.Pattern
 
-class Mangasee(override val id: Int) : ParsedOnlineSource() {
+class Mangasee : ParsedOnlineSource() {
+
+    override val id: Long = 9
 
     override val name = "Mangasee"
 
@@ -30,46 +27,32 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
 
     private val indexPattern = Pattern.compile("-index-(.*?)-")
 
-    override fun popularMangaInitialUrl() = "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending&todo=1"
-
     override fun popularMangaSelector() = "div.requested > div.row"
 
-    override fun popularMangaRequest(page: MangasPage): Request {
-        if (page.page == 1) {
-            page.url = popularMangaInitialUrl()
-        }
-        val (body, requestUrl) = convertQueryToPost(page)
+    override fun popularMangaRequest(page: Int): Request {
+        val (body, requestUrl) = convertQueryToPost(page, "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending&todo=1")
         return POST(requestUrl, headers, body.build())
     }
 
-    override fun popularMangaParse(response: Response, page: MangasPage) {
-        val document = response.asJsoup()
-        for (element in document.select(popularMangaSelector())) {
-            Manga.create(id).apply {
-                popularMangaFromElement(element, this)
-                page.mangas.add(this)
-            }
-        }
-
-        page.nextPageUrl = page.url
-    }
-
-    override fun popularMangaFromElement(element: Element, manga: Manga) {
+    override fun popularMangaFromElement(element: Element): SManga {
+        val manga = SManga.create()
         element.select("a.resultLink").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.text()
         }
+        return manga
     }
 
-    // Not used, overrides parent.
-    override fun popularMangaNextPageSelector() = ""
+    override fun popularMangaNextPageSelector() = "button.requestMore"
+
+    override fun searchMangaSelector() = "div.requested > div.row"
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String {
+    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
         val url = HttpUrl.parse("$baseUrl/search/request.php").newBuilder()
         if (!query.isEmpty()) url.addQueryParameter("keyword", query)
         var genres: String? = null
         var genresNo: String? = null
-        for (filter in if (filters.isEmpty()) [email protected] else filters) {
+        for (filter in if (filters.isEmpty()) getFilterList() else filters) {
             when (filter) {
                 is Sort -> filter.values[filter.state].keys.forEachIndexed { i, s ->
                     url.addQueryParameter(s, filter.values[filter.state].values[i])
@@ -84,22 +67,14 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
         }
         if (genres != null) url.addQueryParameter("genre", genres)
         if (genresNo != null) url.addQueryParameter("genreNo", genresNo)
-        return url.toString()
-    }
-
-    override fun searchMangaSelector() = "div.searchResults > div.requested > div.row"
 
-    override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): Request {
-        if (page.page == 1) {
-            page.url = searchMangaInitialUrl(query, filters)
-        }
-        val (body, requestUrl) = convertQueryToPost(page)
+        val (body, requestUrl) = convertQueryToPost(page, url.toString())
         return POST(requestUrl, headers, body.build())
     }
 
-    private fun convertQueryToPost(page: MangasPage): Pair<FormBody.Builder, String> {
-        val url = HttpUrl.parse(page.url)
-        val body = FormBody.Builder().add("page", page.page.toString())
+    private fun convertQueryToPost(page: Int, url: String): Pair<FormBody.Builder, String> {
+        val url = HttpUrl.parse(url)
+        val body = FormBody.Builder().add("page", page.toString())
         for (i in 0..url.querySize() - 1) {
             body.add(url.queryParameterName(i), url.queryParameterValue(i))
         }
@@ -107,63 +82,57 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
         return Pair(body, requestUrl)
     }
 
-    override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) {
-        val document = response.asJsoup()
-        for (element in document.select(popularMangaSelector())) {
-            Manga.create(id).apply {
-                popularMangaFromElement(element, this)
-                page.mangas.add(this)
-            }
-        }
-
-        page.nextPageUrl = page.url
-    }
-
-    override fun searchMangaFromElement(element: Element, manga: Manga) {
+    override fun searchMangaFromElement(element: Element): SManga {
+        val manga = SManga.create()
         element.select("a.resultLink").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.text()
         }
+        return manga
     }
 
-    // Not used, overrides parent.
-    override fun searchMangaNextPageSelector() = ""
+    override fun searchMangaNextPageSelector() = "button.requestMore"
 
-    override fun mangaDetailsParse(document: Document, manga: Manga) {
+    override fun mangaDetailsParse(document: Document): SManga {
         val detailElement = document.select("div.well > div.row").first()
 
+        val manga = SManga.create()
         manga.author = detailElement.select("a[href^=/search/?author=]").first()?.text()
         manga.genre = detailElement.select("span.details > div.row > div:has(b:contains(Genre(s))) > a").map { it.text() }.joinToString()
         manga.description = detailElement.select("strong:contains(Description:) + div").first()?.text()
         manga.status = detailElement.select("a[href^=/search/?status=]").first()?.text().orEmpty().let { parseStatus(it) }
         manga.thumbnail_url = detailElement.select("div > img").first()?.absUrl("src")
+        return manga
     }
 
     private fun parseStatus(status: String) = when {
-        status.contains("Ongoing (Scan)") -> Manga.ONGOING
-        status.contains("Complete (Scan)") -> Manga.COMPLETED
-        else -> Manga.UNKNOWN
+        status.contains("Ongoing (Scan)") -> SManga.ONGOING
+        status.contains("Complete (Scan)") -> SManga.COMPLETED
+        else -> SManga.UNKNOWN
     }
 
     override fun chapterListSelector() = "div.chapter-list > a"
 
-    override fun chapterFromElement(element: Element, chapter: Chapter) {
+    override fun chapterFromElement(element: Element): SChapter {
         val urlElement = element.select("a").first()
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.name = element.select("span.chapterLabel").first().text()?.let { it } ?: ""
         chapter.date_upload = element.select("time").first()?.attr("datetime")?.let { parseChapterDate(it) } ?: 0
+        return chapter
     }
 
     private fun parseChapterDate(dateAsString: String): Long {
         return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(dateAsString).time
     }
 
-    override fun pageListParse(response: Response, pages: MutableList<Page>) {
-        val document = response.asJsoup()
-        val fullUrl = response.request().url().toString()
+    override fun pageListParse(document: Document): List<Page> {
+        val fullUrl = document.baseUri()
         val url = fullUrl.substringBeforeLast('/')
 
+        val pages = mutableListOf<Page>()
+
         val series = document.select("input.IndexName").first().attr("value")
         val chapter = document.select("span.CurChapter").first().text()
         var index = ""
@@ -178,10 +147,7 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
             pages.add(Page(pages.size, "$url/$series-chapter-$chapter$index-page-${pages.size + 1}.html"))
         }
         pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
-    }
-
-    // Not used, overrides parent.
-    override fun pageListParse(document: Document, pages: MutableList<Page>) {
+        return pages
     }
 
     override fun imageUrlParse(document: Document): String = document.select("img.CurImage").attr("src")
@@ -197,7 +163,7 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
 
     // [...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(
+    override fun getFilterList() = FilterList(
             TextField("Years", "year"),
             TextField("Author", "author"),
             Sort("Sort By", arrayOf(SortOption("Alphabetical A-Z", emptyArray(), emptyArray()),
@@ -249,34 +215,18 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
             Genre("Yuri")
     )
 
-    override fun latestUpdatesInitialUrl(): String = "http://mangaseeonline.net/home/latest.request.php"
-
-    // Not used, overrides parent.
-    override fun latestUpdatesNextPageSelector(): String = ""
+    override fun latestUpdatesNextPageSelector() = "button.requestMore"
 
     override fun latestUpdatesSelector(): String = "a.latestSeries"
 
-    override fun latestUpdatesRequest(page: MangasPage): Request {
-        if (page.page == 1) {
-            page.url = latestUpdatesInitialUrl()
-        }
-        val (body, requestUrl) = convertQueryToPost(page)
+    override fun latestUpdatesRequest(page: Int): Request {
+        val url = "http://mangaseeonline.net/home/latest.request.php"
+        val (body, requestUrl) = convertQueryToPost(page, url)
         return POST(requestUrl, headers, body.build())
     }
 
-    override fun latestUpdatesParse(response: Response, page: MangasPage) {
-        val document = response.asJsoup()
-        for (element in document.select(latestUpdatesSelector())) {
-            Manga.create(id).apply {
-                latestUpdatesFromElement(element, this)
-                page.mangas.add(this)
-            }
-        }
-
-        page.nextPageUrl = page.url
-    }
-
-    override fun latestUpdatesFromElement(element: Element, manga: Manga) {
+    override fun latestUpdatesFromElement(element: Element): SManga {
+        val manga = SManga.create()
         element.select("a.latestSeries").first().let {
             val chapterUrl = it.attr("href")
             val indexOfMangaUrl = chapterUrl.indexOf("-chapter-")
@@ -288,6 +238,7 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
             manga.setUrlWithoutDomain("/manga" + mangaUrl)
             manga.title = title
         }
+        return manga
     }
 
 }

+ 38 - 33
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Readmangatoday.kt

@@ -1,10 +1,8 @@
 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.GET
 import eu.kanade.tachiyomi.data.network.POST
-import eu.kanade.tachiyomi.data.source.model.MangasPage
-import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.data.source.model.*
 import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
 import okhttp3.Headers
 import okhttp3.OkHttpClient
@@ -13,7 +11,9 @@ import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
 import java.util.*
 
-class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
+class Readmangatoday : ParsedOnlineSource() {
+
+    override val id: Long = 8
 
     override val name = "ReadMangaToday"
 
@@ -33,41 +33,39 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
         add("X-Requested-With", "XMLHttpRequest")
     }
 
-    override fun popularMangaInitialUrl() = "$baseUrl/hot-manga/"
+    override fun popularMangaRequest(page: Int): Request {
+        return GET("$baseUrl/hot-manga/$page", headers)
+    }
 
-    override fun latestUpdatesInitialUrl() = "$baseUrl/latest-releases/"
+    override fun latestUpdatesRequest(page: Int): Request {
+        return GET("$baseUrl/latest-releases/$page", headers)
+    }
 
     override fun popularMangaSelector() = "div.hot-manga > div.style-list > div.box"
 
     override fun latestUpdatesSelector() = "div.hot-manga > div.style-grid > div.box"
 
-    override fun popularMangaFromElement(element: Element, manga: Manga) {
+    override fun popularMangaFromElement(element: Element): SManga {
+        val manga = SManga.create()
         element.select("div.title > h2 > a").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.attr("title")
         }
+        return manga
     }
 
-    override fun latestUpdatesFromElement(element: Element, manga: Manga) {
-        popularMangaFromElement(element, manga)
+    override fun latestUpdatesFromElement(element: Element): SManga {
+        return popularMangaFromElement(element)
     }
 
     override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
 
-    override fun latestUpdatesNextPageSelector(): String = "div.hot-manga > ul.pagination > li > a:contains(»)"
-
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) =
-            "$baseUrl/service/advanced_search"
-
-
-    override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): Request {
-        if (page.page == 1) {
-            page.url = searchMangaInitialUrl(query, filters)
-        }
+    override fun latestUpdatesNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
 
+    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
         val builder = okhttp3.FormBody.Builder()
         builder.add("manga-name", query)
-        for (filter in if (filters.isEmpty()) [email protected] else filters) {
+        (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
             when (filter) {
                 is TextField -> builder.add(filter.key, filter.state)
                 is Type -> builder.add("type", arrayOf("all", "japanese", "korean", "chinese")[filter.state])
@@ -75,49 +73,54 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
                 is Genre -> when (filter.state) {
                     Filter.TriState.STATE_INCLUDE -> builder.add("include[]", filter.id.toString())
                     Filter.TriState.STATE_EXCLUDE -> builder.add("exclude[]", filter.id.toString())
-
                 }
             }
         }
-        return POST(page.url, headers, builder.build())
+        return POST("$baseUrl/service/advanced_search", headers, builder.build())
     }
 
     override fun searchMangaSelector() = "div.style-list > div.box"
 
-    override fun searchMangaFromElement(element: Element, manga: Manga) {
+    override fun searchMangaFromElement(element: Element): SManga {
+        val manga = SManga.create()
         element.select("div.title > h2 > a").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.attr("title")
         }
+        return manga
     }
 
     override fun searchMangaNextPageSelector() = "div.next-page > a.next"
 
-    override fun mangaDetailsParse(document: Document, manga: Manga) {
+    override fun mangaDetailsParse(document: Document): SManga {
         val detailElement = document.select("div.movie-meta").first()
 
+        val manga = SManga.create()
         manga.author = document.select("ul.cast-list li.director > ul a").first()?.text()
         manga.artist = document.select("ul.cast-list li:not(.director) > ul a").first()?.text()
         manga.genre = detailElement.select("dl.dl-horizontal > dd:eq(5)").first()?.text()
         manga.description = detailElement.select("li.movie-detail").first()?.text()
         manga.status = detailElement.select("dl.dl-horizontal > dd:eq(3)").first()?.text().orEmpty().let { parseStatus(it) }
         manga.thumbnail_url = detailElement.select("img.img-responsive").first()?.attr("src")
+        return manga
     }
 
     private fun parseStatus(status: String) = when {
-        status.contains("Ongoing") -> Manga.ONGOING
-        status.contains("Completed") -> Manga.COMPLETED
-        else -> Manga.UNKNOWN
+        status.contains("Ongoing") -> SManga.ONGOING
+        status.contains("Completed") -> SManga.COMPLETED
+        else -> SManga.UNKNOWN
     }
 
     override fun chapterListSelector() = "ul.chp_lst > li"
 
-    override fun chapterFromElement(element: Element, chapter: Chapter) {
+    override fun chapterFromElement(element: Element): SChapter {
         val urlElement = element.select("a").first()
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.name = urlElement.select("span.val").text()
         chapter.date_upload = element.select("span.dte").first()?.text()?.let { parseChapterDate(it) } ?: 0
+        return chapter
     }
 
     private fun parseChapterDate(date: String): Long {
@@ -125,7 +128,7 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
 
         if (dateWords.size == 3) {
             val timeAgo = Integer.parseInt(dateWords[0])
-            var date: Calendar = Calendar.getInstance()
+            val date: Calendar = Calendar.getInstance()
 
             if (dateWords[1].contains("Minute")) {
                 date.add(Calendar.MINUTE, -timeAgo)
@@ -141,17 +144,19 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
                 date.add(Calendar.YEAR, -timeAgo)
             }
 
-            return date.getTimeInMillis()
+            return date.timeInMillis
         }
 
         return 0L
     }
 
-    override fun pageListParse(document: Document, pages: MutableList<Page>) {
+    override fun pageListParse(document: Document): List<Page> {
+        val pages = mutableListOf<Page>()
         document.select("ul.list-switcher-2 > li > select.jump-menu").first().getElementsByTag("option").forEach {
             pages.add(Page(pages.size, it.attr("value")))
         }
         pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
+        return pages
     }
 
     override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src")
@@ -163,7 +168,7 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
 
     // [...document.querySelectorAll("ul.manga-cat span")].map(el => `Genre("${el.nextSibling.textContent.trim()}", ${el.getAttribute('data-id')})`).join(',\n')
     // http://www.readmanga.today/advanced-search
-    override fun getFilterList(): List<Filter<*>> = listOf(
+    override fun getFilterList() = FilterList(
             TextField("Author", "author-name"),
             TextField("Artist", "artist-name"),
             Type(),

+ 36 - 21
app/src/main/java/eu/kanade/tachiyomi/data/source/online/german/WieManga.kt

@@ -1,16 +1,19 @@
 package eu.kanade.tachiyomi.data.source.online.german
 
-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.source.model.FilterList
 import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.data.source.model.SChapter
+import eu.kanade.tachiyomi.data.source.model.SManga
 import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
-import eu.kanade.tachiyomi.util.asJsoup
-import okhttp3.Response
+import okhttp3.Request
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
 import java.text.SimpleDateFormat
 
-class WieManga(override val id: Int) : ParsedOnlineSource() {
+class WieManga : ParsedOnlineSource() {
+
+    override val id: Long = 10
 
     override val name = "Wie Manga!"
 
@@ -20,50 +23,61 @@ class WieManga(override val id: Int) : ParsedOnlineSource() {
 
     override val supportsLatest = true
 
-    override fun popularMangaInitialUrl() = "$baseUrl/list/Hot-Book/"
-
-    override fun latestUpdatesInitialUrl() = "$baseUrl/list/New-Update/"
-
     override fun popularMangaSelector() = ".booklist td > div"
 
     override fun latestUpdatesSelector() = ".booklist td > div"
 
-    override fun popularMangaFromElement(element: Element, manga: Manga) {
+    override fun popularMangaRequest(page: Int): Request {
+        return GET("$baseUrl/list/Hot-Book/", headers)
+    }
+
+    override fun latestUpdatesRequest(page: Int): Request {
+        return GET("$baseUrl/list/New-Update/", headers)
+    }
+
+    override fun popularMangaFromElement(element: Element): SManga {
         val image = element.select("dt img")
         val title = element.select("dd a:first-child")
 
+        val manga = SManga.create()
         manga.setUrlWithoutDomain(title.attr("href"))
         manga.title = title.text()
         manga.thumbnail_url = image.attr("src")
+        return manga
     }
 
-    override fun latestUpdatesFromElement(element: Element, manga: Manga) {
-        popularMangaFromElement(element, manga)
+    override fun latestUpdatesFromElement(element: Element): SManga {
+        return popularMangaFromElement(element)
     }
 
     override fun popularMangaNextPageSelector() = null
 
     override fun latestUpdatesNextPageSelector() = null
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) = "$baseUrl/search/?wd=$query"
+    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+        return GET("$baseUrl/search/?wd=$query", headers)
+    }
 
     override fun searchMangaSelector() = ".searchresult td > div"
 
-    override fun searchMangaFromElement(element: Element, manga: Manga) {
+    override fun searchMangaFromElement(element: Element): SManga {
         val image = element.select(".resultimg img")
         val title = element.select(".resultbookname")
 
+        val manga = SManga.create()
         manga.setUrlWithoutDomain(title.attr("href"))
         manga.title = title.text()
         manga.thumbnail_url = image.attr("src")
+        return manga
     }
 
     override fun searchMangaNextPageSelector() = ".pagetor a.l"
 
-    override fun mangaDetailsParse(document: Document, manga: Manga) {
+    override fun mangaDetailsParse(document: Document): SManga {
         val imageElement = document.select(".bookmessgae tr > td:nth-child(1)").first()
         val infoElement = document.select(".bookmessgae tr > td:nth-child(2)").first()
 
+        val manga = SManga.create()
         manga.author = infoElement.select("dd:nth-of-type(2) a").first()?.text()
         manga.artist = infoElement.select("dd:nth-of-type(3) a").first()?.text()
         manga.description = infoElement.select("dl > dt:last-child").first()?.text()?.replaceFirst("Beschreibung", "")
@@ -74,32 +88,33 @@ class WieManga(override val id: Int) : ParsedOnlineSource() {
 
         if (manga.artist == "RSS")
             manga.artist = null
+        return manga
     }
 
     override fun chapterListSelector() = ".chapterlist tr:not(:first-child)"
 
-    override fun chapterFromElement(element: Element, chapter: Chapter) {
+    override fun chapterFromElement(element: Element): SChapter {
         val urlElement = element.select(".col1 a").first()
         val dateElement = element.select(".col3 a").first()
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.name = urlElement.text()
         chapter.date_upload = dateElement?.text()?.let { parseChapterDate(it) } ?: 0
+        return chapter
     }
 
     private fun parseChapterDate(date: String): Long {
         return SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(date).time
     }
 
-    override fun pageListParse(response: Response, pages: MutableList<Page>) {
-        val document = response.asJsoup()
+    override fun pageListParse(document: Document): List<Page> {
+        val pages = mutableListOf<Page>()
 
         document.select("select#page").first().select("option").forEach {
             pages.add(Page(pages.size, it.attr("value")))
         }
-    }
-
-    override fun pageListParse(document: Document, pages: MutableList<Page>) {
+        return pages
     }
 
     override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src")

+ 64 - 48
app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mangachan.kt

@@ -1,18 +1,19 @@
 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.model.MangasPage
-import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.data.network.GET
+import eu.kanade.tachiyomi.data.source.model.*
 import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
 import eu.kanade.tachiyomi.util.asJsoup
+import okhttp3.Request
 import okhttp3.Response
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
 import java.text.SimpleDateFormat
 import java.util.*
 
-class Mangachan(override val id: Int) : ParsedOnlineSource() {
+class Mangachan : ParsedOnlineSource() {
+
+    override val id: Long = 7
 
     override val name = "Mangachan"
 
@@ -22,23 +23,28 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
 
     override val supportsLatest = true
 
-    override fun popularMangaInitialUrl() = "$baseUrl/mostfavorites"
-
-    override fun latestUpdatesInitialUrl() = "$baseUrl/newestch"
+    override fun popularMangaRequest(page: Int): Request {
+        return GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
+    }
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String {
-        if (query.isNotEmpty()) {
-            return "$baseUrl/?do=search&subaction=search&story=$query"
+    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+        val url = if (query.isNotEmpty()) {
+            "$baseUrl/?do=search&subaction=search&story=$query"
         } else {
-            val filt = filters.filter { it.state != Filter.TriState.STATE_IGNORE }
+            val filt = filters.filterIsInstance<Genre>().filter { !it.isIgnored() }
             if (filt.isNotEmpty()) {
                 var genres = ""
-                filt.forEach { genres += (if (it.state == Filter.TriState.STATE_EXCLUDE) "-" else "") + (it as Genre).id + '+' }
-                return "$baseUrl/tags/${genres.dropLast(1)}"
+                filt.forEach { genres += (if (it.isExcluded()) "-" else "") + it.id + '+' }
+                "$baseUrl/tags/${genres.dropLast(1)}"
             } else {
-                return "$baseUrl/?do=search&subaction=search&story=$query"
+                "$baseUrl/?do=search&subaction=search&story=$query"
             }
         }
+        return GET(url, headers)
+    }
+
+    override fun latestUpdatesRequest(page: Int): Request {
+        return GET("$baseUrl/newestch?page=$page")
     }
 
     override fun popularMangaSelector() = "div.content_row"
@@ -47,22 +53,26 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
 
     override fun searchMangaSelector() = popularMangaSelector()
 
-    override fun popularMangaFromElement(element: Element, manga: Manga) {
+    override fun popularMangaFromElement(element: Element): SManga {
+        val manga = SManga.create()
         element.select("h2 > a").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.text()
         }
+        return manga
     }
 
-    override fun latestUpdatesFromElement(element: Element, manga: Manga) {
+    override fun latestUpdatesFromElement(element: Element): SManga {
+        val manga = SManga.create()
         element.select("a:nth-child(1)").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.text()
         }
+        return manga
     }
 
-    override fun searchMangaFromElement(element: Element, manga: Manga) {
-        popularMangaFromElement(element, manga)
+    override fun searchMangaFromElement(element: Element): SManga {
+        return popularMangaFromElement(element)
     }
 
     override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
@@ -73,74 +83,80 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
 
     private fun searchGenresNextPageSelector() = popularMangaNextPageSelector()
 
-    override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) {
+    override fun searchMangaParse(response: Response): MangasPage {
         val document = response.asJsoup()
-        for (element in document.select(searchMangaSelector())) {
-            Manga.create(id).apply {
-                searchMangaFromElement(element, this)
-                page.mangas.add(this)
-            }
-        }
-        val allIgnore = filters.all { it.state == Filter.TriState.STATE_IGNORE }
-        searchMangaNextPageSelector().let { selector ->
-            if (page.nextPageUrl.isNullOrEmpty() && allIgnore) {
-                val onClick = document.select(selector).first()?.attr("onclick")
-                val pageNum = onClick?.substring(23, onClick.indexOf("); return(false)"))
-                page.nextPageUrl = searchMangaInitialUrl(query, emptyList()) + "&search_start=" + pageNum
-            }
+        val mangas = document.select(searchMangaSelector()).map { element ->
+            searchMangaFromElement(element)
         }
 
-        searchGenresNextPageSelector().let { selector ->
-            if (page.nextPageUrl.isNullOrEmpty() && !allIgnore) {
-                val url = document.select(selector).first()?.attr("href")
-                page.nextPageUrl = searchMangaInitialUrl(query, filters) + url
-            }
-        }
+        // FIXME
+//        val allIgnore = filters.all { it.state == Filter.TriState.STATE_IGNORE }
+//        searchMangaNextPageSelector().let { selector ->
+//            if (page.nextPageUrl.isNullOrEmpty() && allIgnore) {
+//                val onClick = document.select(selector).first()?.attr("onclick")
+//                val pageNum = onClick?.substring(23, onClick.indexOf("); return(false)"))
+//                page.nextPageUrl = searchMangaInitialUrl(query, emptyList()) + "&search_start=" + pageNum
+//            }
+//        }
+//
+//        searchGenresNextPageSelector().let { selector ->
+//            if (page.nextPageUrl.isNullOrEmpty() && !allIgnore) {
+//                val url = document.select(selector).first()?.attr("href")
+//                page.nextPageUrl = searchMangaInitialUrl(query, filters) + url
+//            }
+//        }
+
+        return MangasPage(mangas, false)
     }
 
-    override fun mangaDetailsParse(document: Document, manga: Manga) {
+    override fun mangaDetailsParse(document: Document): SManga {
         val infoElement = document.select("table.mangatitle").first()
         val descElement = document.select("div#description").first()
         val imgElement = document.select("img#cover").first()
 
+        val manga = SManga.create()
         manga.author = infoElement.select("tr:eq(2) > td:eq(1)").text()
         manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text()
         manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text())
         manga.description = descElement.textNodes().first().text()
         manga.thumbnail_url = baseUrl + imgElement.attr("src")
+        return manga
     }
 
     private fun parseStatus(element: String): Int {
         when {
-            element.contains("перевод завершен") -> return Manga.COMPLETED
-            element.contains("перевод продолжается") -> return Manga.ONGOING
-            else -> return Manga.UNKNOWN
+            element.contains("перевод завершен") -> return SManga.COMPLETED
+            element.contains("перевод продолжается") -> return SManga.ONGOING
+            else -> return SManga.UNKNOWN
         }
     }
 
     override fun chapterListSelector() = "table.table_cha tr:gt(1)"
 
-    override fun chapterFromElement(element: Element, chapter: Chapter) {
+    override fun chapterFromElement(element: Element): SChapter {
         val urlElement = element.select("a").first()
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.name = urlElement.text()
         chapter.date_upload = element.select("div.date").first()?.text()?.let {
             SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it).time
         } ?: 0
+        return chapter
     }
 
-    override fun pageListParse(response: Response, pages: MutableList<Page>) {
+    override fun pageListParse(response: Response): List<Page> {
         val html = response.body().string()
         val beginIndex = html.indexOf("fullimg\":[") + 10
         val endIndex = html.indexOf(",]", beginIndex)
         val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "")
         val pageUrls = trimmedHtml.split(',')
 
-        pageUrls.mapIndexedTo(pages) { i, url -> Page(i, "", url) }
+        return pageUrls.mapIndexed { i, url -> Page(i, "", url) }
     }
 
-    override fun pageListParse(document: Document, pages: MutableList<Page>) {
+    override fun pageListParse(document: Document): List<Page> {
+        throw Exception("Not used")
     }
 
     override fun imageUrlParse(document: Document) = ""
@@ -152,7 +168,7 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
     *  return `Genre("${id.replace("_", " ")}")` }).join(',\n')
     *  on http://mangachan.me/
     */
-    override fun getFilterList(): List<Filter<*>> = listOf(
+    override fun getFilterList() = FilterList(
             Genre("18 плюс"),
             Genre("bdsm"),
             Genre("арт"),

+ 42 - 24
app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mintmanga.kt

@@ -1,9 +1,9 @@
 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.model.Page
+import eu.kanade.tachiyomi.data.network.GET
+import eu.kanade.tachiyomi.data.source.model.*
 import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
+import okhttp3.Request
 import okhttp3.Response
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
@@ -11,7 +11,9 @@ import java.text.SimpleDateFormat
 import java.util.*
 import java.util.regex.Pattern
 
-class Mintmanga(override val id: Int) : ParsedOnlineSource() {
+class Mintmanga : ParsedOnlineSource() {
+
+    override val id: Long = 6
 
     override val name = "Mintmanga"
 
@@ -21,77 +23,89 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() {
 
     override val supportsLatest = true
 
-    override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
-
-    override fun latestUpdatesInitialUrl() = "$baseUrl/list?sortType=updated"
+    override fun popularMangaRequest(page: Int): Request {
+        return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
+    }
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) =
-            "$baseUrl/search?q=$query&${filters.map { (it as Genre).id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")}"
+    override fun latestUpdatesRequest(page: Int): Request {
+        return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
+    }
 
     override fun popularMangaSelector() = "div.desc"
 
     override fun latestUpdatesSelector() = "div.desc"
 
-    override fun popularMangaFromElement(element: Element, manga: Manga) {
+    override fun popularMangaFromElement(element: Element): SManga {
+        val manga = SManga.create()
         element.select("h3 > a").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.attr("title")
         }
+        return manga
     }
 
-    override fun latestUpdatesFromElement(element: Element, manga: Manga) {
-        popularMangaFromElement(element, manga)
+    override fun latestUpdatesFromElement(element: Element): SManga {
+        return popularMangaFromElement(element)
     }
 
     override fun popularMangaNextPageSelector() = "a.nextLink"
 
     override fun latestUpdatesNextPageSelector() = "a.nextLink"
 
+    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+        val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")
+        return GET("$baseUrl/search?q=$query&$genres", headers)
+    }
+
     override fun searchMangaSelector() = popularMangaSelector()
 
-    override fun searchMangaFromElement(element: Element, manga: Manga) {
-        popularMangaFromElement(element, manga)
+    override fun searchMangaFromElement(element: Element): SManga {
+        return popularMangaFromElement(element)
     }
 
     // max 200 results
     override fun searchMangaNextPageSelector() = null
 
-    override fun mangaDetailsParse(document: Document, manga: Manga) {
+    override fun mangaDetailsParse(document: Document): SManga {
         val infoElement = document.select("div.leftContent").first()
 
+        val manga = SManga.create()
         manga.author = infoElement.select("span.elem_author").first()?.text()
         manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",")
         manga.description = infoElement.select("div.manga-description").text()
         manga.status = parseStatus(infoElement.html())
         manga.thumbnail_url = infoElement.select("img").attr("data-full")
+        return manga
     }
 
     private fun parseStatus(element: String): Int {
         when {
-            element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return Manga.LICENSED
-            element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return Manga.COMPLETED
-            element.contains("<b>Перевод:</b> продолжается") -> return Manga.ONGOING
-            else -> return Manga.UNKNOWN
+            element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED
+            element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED
+            element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING
+            else -> return SManga.UNKNOWN
         }
     }
 
     override fun chapterListSelector() = "div.chapters-link tbody tr"
 
-    override fun chapterFromElement(element: Element, chapter: Chapter) {
+    override fun chapterFromElement(element: Element): SChapter {
         val urlElement = element.select("a").first()
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1")
         chapter.name = urlElement.text().replace(" новое", "")
         chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
             SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
         } ?: 0
+        return chapter
     }
 
-    override fun prepareNewChapter(chapter: Chapter, manga: Manga) {
+    override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
         chapter.chapter_number = -2f
     }
 
-    override fun pageListParse(response: Response, pages: MutableList<Page>) {
+    override fun pageListParse(response: Response): List<Page> {
         val html = response.body().string()
         val beginIndex = html.indexOf("rm_h.init( [")
         val endIndex = html.indexOf("], 0, false);", beginIndex)
@@ -100,14 +114,18 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() {
         val p = Pattern.compile("'.+?','.+?',\".+?\"")
         val m = p.matcher(trimmedHtml)
 
+        val pages = mutableListOf<Page>()
+
         var i = 0
         while (m.find()) {
             val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
             pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
         }
+        return pages
     }
 
-    override fun pageListParse(document: Document, pages: MutableList<Page>) {
+    override fun pageListParse(document: Document): List<Page> {
+        throw Exception("Not used")
     }
 
     override fun imageUrlParse(document: Document) = ""
@@ -119,7 +137,7 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() {
     *  return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
     *  on http://mintmanga.com/search
     */
-    override fun getFilterList(): List<Filter<*>> = listOf(
+    override fun getFilterList() = FilterList(
             Genre("арт", "el_2220"),
             Genre("бара", "el_1353"),
             Genre("боевик", "el_1346"),

+ 44 - 26
app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Readmanga.kt

@@ -1,9 +1,9 @@
 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.model.Page
+import eu.kanade.tachiyomi.data.network.GET
+import eu.kanade.tachiyomi.data.source.model.*
 import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
+import okhttp3.Request
 import okhttp3.Response
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
@@ -11,7 +11,9 @@ import java.text.SimpleDateFormat
 import java.util.*
 import java.util.regex.Pattern
 
-class Readmanga(override val id: Int) : ParsedOnlineSource() {
+class Readmanga : ParsedOnlineSource() {
+
+    override val id: Long = 5
 
     override val name = "Readmanga"
 
@@ -21,77 +23,89 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() {
 
     override val supportsLatest = true
 
-    override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
-
-    override fun latestUpdatesInitialUrl() = "$baseUrl/list?sortType=updated"
-
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) =
-            "$baseUrl/search?q=$query&${filters.map { (it as Genre).id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")}"
-
     override fun popularMangaSelector() = "div.desc"
 
     override fun latestUpdatesSelector() = "div.desc"
 
-    override fun popularMangaFromElement(element: Element, manga: Manga) {
+    override fun popularMangaRequest(page: Int): Request {
+        return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
+    }
+
+    override fun latestUpdatesRequest(page: Int): Request {
+        return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
+    }
+
+    override fun popularMangaFromElement(element: Element): SManga {
+        val manga = SManga.create()
         element.select("h3 > a").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.attr("title")
         }
+        return manga
     }
 
-    override fun latestUpdatesFromElement(element: Element, manga: Manga) {
-        popularMangaFromElement(element, manga)
+    override fun latestUpdatesFromElement(element: Element): SManga {
+        return popularMangaFromElement(element)
     }
 
     override fun popularMangaNextPageSelector() = "a.nextLink"
 
     override fun latestUpdatesNextPageSelector() = "a.nextLink"
 
+    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+        val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")
+        return GET("$baseUrl/search?q=$query&$genres", headers)
+    }
+
     override fun searchMangaSelector() = popularMangaSelector()
 
-    override fun searchMangaFromElement(element: Element, manga: Manga) {
-        popularMangaFromElement(element, manga)
+    override fun searchMangaFromElement(element: Element): SManga {
+        return popularMangaFromElement(element)
     }
 
     // max 200 results
     override fun searchMangaNextPageSelector() = null
 
-    override fun mangaDetailsParse(document: Document, manga: Manga) {
+    override fun mangaDetailsParse(document: Document): SManga {
         val infoElement = document.select("div.leftContent").first()
 
+        val manga = SManga.create()
         manga.author = infoElement.select("span.elem_author").first()?.text()
         manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",")
         manga.description = infoElement.select("div.manga-description").text()
         manga.status = parseStatus(infoElement.html())
         manga.thumbnail_url = infoElement.select("img").attr("data-full")
+        return manga
     }
 
     private fun parseStatus(element: String): Int {
         when {
-            element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return Manga.LICENSED
-            element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return Manga.COMPLETED
-            element.contains("<b>Перевод:</b> продолжается") -> return Manga.ONGOING
-            else -> return Manga.UNKNOWN
+            element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED
+            element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED
+            element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING
+            else -> return SManga.UNKNOWN
         }
     }
 
     override fun chapterListSelector() = "div.chapters-link tbody tr"
 
-    override fun chapterFromElement(element: Element, chapter: Chapter) {
+    override fun chapterFromElement(element: Element): SChapter {
         val urlElement = element.select("a").first()
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1")
         chapter.name = urlElement.text().replace(" новое", "")
         chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
             SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
         } ?: 0
+        return chapter
     }
 
-    override fun prepareNewChapter(chapter: Chapter, manga: Manga) {
+    override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
         chapter.chapter_number = -2f
     }
 
-    override fun pageListParse(response: Response, pages: MutableList<Page>) {
+    override fun pageListParse(response: Response): List<Page> {
         val html = response.body().string()
         val beginIndex = html.indexOf("rm_h.init( [")
         val endIndex = html.indexOf("], 0, false);", beginIndex)
@@ -100,14 +114,18 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() {
         val p = Pattern.compile("'.+?','.+?',\".+?\"")
         val m = p.matcher(trimmedHtml)
 
+        val pages = mutableListOf<Page>()
+
         var i = 0
         while (m.find()) {
             val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
             pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
         }
+        return pages
     }
 
-    override fun pageListParse(document: Document, pages: MutableList<Page>) {
+    override fun pageListParse(document: Document): List<Page> {
+        throw Exception("Not used")
     }
 
     override fun imageUrlParse(document: Document) = ""
@@ -119,7 +137,7 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() {
     *  return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
     *  on http://readmanga.me/search
     */
-    override fun getFilterList(): List<Filter<*>> = listOf(
+    override fun getFilterList() = FilterList(
             Genre("арт", "el_5685"),
             Genre("боевик", "el_2155"),
             Genre("боевые искусства", "el_2143"),

+ 22 - 13
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt

@@ -14,6 +14,7 @@ import com.afollestad.materialdialogs.MaterialDialog
 import com.f2prateek.rx.preferences.Preference
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.source.model.FilterList
 import eu.kanade.tachiyomi.data.source.online.LoginSource
 import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
 import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
@@ -32,7 +33,6 @@ import nucleus.factory.RequiresPresenter
 import rx.Subscription
 import rx.android.schedulers.AndroidSchedulers
 import rx.subjects.PublishSubject
-import timber.log.Timber
 import java.util.concurrent.TimeUnit.MILLISECONDS
 
 /**
@@ -104,6 +104,11 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
     private val toolbar: Toolbar
         get() = (activity as MainActivity).toolbar
 
+    /**
+     * Snackbar containing an error message when a request fails.
+     */
+    private var snack: Snackbar? = null
+
     /**
      * Navigation view containing filter items.
      */
@@ -201,8 +206,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
             } else if (source != presenter.source) {
                 selectedIndex = position
                 showProgressBar()
-                glm.scrollToPositionWithOffset(0, 0)
-                llm.scrollToPositionWithOffset(0, 0)
+                adapter.clear()
                 presenter.setActiveSource(source)
                 navView?.setFilters(presenter.sourceFilters)
                 activity.invalidateOptionsMenu()
@@ -233,14 +237,14 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
         }
 
         navView.onSearchClicked = {
-            val allDefault = (0..navView.adapter.items.lastIndex)
-                    .none { navView.adapter.items[it].state != presenter.source.filters[it].state }
-
-            presenter.setSourceFilter(if (allDefault) emptyList() else navView.adapter.items)
+            val allDefault = navView.adapter.items.hasSameState(presenter.source.getFilterList())
+            showProgressBar()
+            adapter.clear()
+            presenter.setSourceFilter(if (allDefault) FilterList() else navView.adapter.items)
         }
 
         navView.onResetClicked = {
-            presenter.appliedFilters = emptyList()
+            presenter.appliedFilters = FilterList()
             val newFilters = presenter.source.getFilterList()
             presenter.sourceFilters = newFilters
             navView.setFilters(newFilters)
@@ -277,7 +281,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
         // Setup filters button
         menu.findItem(R.id.action_set_filter).apply {
             icon.mutate()
-            if (presenter.source.filters.isEmpty()) {
+            if (presenter.sourceFilters.isEmpty()) {
                 isEnabled = false
                 icon.alpha = 128
             } else {
@@ -355,8 +359,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
             return
 
         showProgressBar()
-        catalogue_grid.layoutManager.scrollToPosition(0)
-        catalogue_list.layoutManager.scrollToPosition(0)
+        adapter.clear()
 
         presenter.restartPager(newQuery)
     }
@@ -394,9 +397,11 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
      */
     fun onAddPageError(error: Throwable) {
         hideProgressBar()
-        Timber.e(error)
 
-        catalogue_view.snack(error.message ?: "", Snackbar.LENGTH_INDEFINITE) {
+        val message = if (error is NoResultsException) "No results found" else (error.message ?: "")
+
+        snack?.dismiss()
+        snack = catalogue_view.snack(message, Snackbar.LENGTH_INDEFINITE) {
             setAction(R.string.action_retry) {
                 showProgressBar()
                 presenter.requestNext()
@@ -456,6 +461,8 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
      */
     private fun showProgressBar() {
         progress.visibility = ProgressBar.VISIBLE
+        snack?.dismiss()
+        snack = null
     }
 
     /**
@@ -463,6 +470,8 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
      */
     private fun showGridProgressBar() {
         progress_grid.visibility = ProgressBar.VISIBLE
+        snack?.dismiss()
+        snack = null
     }
 
     /**

+ 4 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt

@@ -9,7 +9,8 @@ import android.view.ViewGroup
 import android.widget.ArrayAdapter
 import android.widget.TextView
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
+import eu.kanade.tachiyomi.data.source.model.Filter
+import eu.kanade.tachiyomi.data.source.model.FilterList
 import eu.kanade.tachiyomi.util.dpToPx
 import eu.kanade.tachiyomi.util.getResourceColor
 import eu.kanade.tachiyomi.util.inflate
@@ -38,14 +39,14 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
         reset_btn.setOnClickListener { onResetClicked() }
     }
 
-    fun setFilters(items: List<Filter<*>>) {
+    fun setFilters(items: FilterList) {
         adapter.items = items
         adapter.notifyDataSetChanged()
     }
 
     inner class Adapter : RecyclerView.Adapter<Holder>() {
 
-        var items: List<Filter<*>> = emptyList()
+        var items: FilterList = FilterList()
 
         override fun getItemCount(): Int {
             return items.size

+ 17 - 13
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt

@@ -1,28 +1,32 @@
 package eu.kanade.tachiyomi.ui.catalogue
 
+import eu.kanade.tachiyomi.data.source.CatalogueSource
+import eu.kanade.tachiyomi.data.source.model.FilterList
 import eu.kanade.tachiyomi.data.source.model.MangasPage
-import eu.kanade.tachiyomi.data.source.online.OnlineSource
-import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
 import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
 
-open class CataloguePager(val source: OnlineSource, val query: String, val filters: List<Filter<*>>) : Pager() {
+open class CataloguePager(val source: CatalogueSource, val query: String, val filters: FilterList) : Pager() {
 
-    override fun requestNext(transformer: (Observable<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage> {
-        val lastPage = lastPage
-
-        val page = if (lastPage == null)
-            MangasPage(1)
-        else
-            MangasPage(lastPage.page + 1).apply { url = lastPage.nextPageUrl!! }
+    override fun requestNext(): Observable<MangasPage> {
+        val page = currentPage
 
         val observable = if (query.isBlank() && filters.isEmpty())
             source.fetchPopularManga(page)
         else
             source.fetchSearchManga(page, query, filters)
 
-        return transformer(observable)
-                .doOnNext { results.onNext(it) }
-                .doOnNext { [email protected] = it }
+        return observable
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .doOnNext {
+                    if (it.mangas.isNotEmpty()) {
+                        onPageReceived(it)
+                    } else {
+                        throw NoResultsException()
+                    }
+                }
     }
 
 }

+ 37 - 50
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt

@@ -6,12 +6,12 @@ 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.CatalogueSource
 import eu.kanade.tachiyomi.data.source.Source
 import eu.kanade.tachiyomi.data.source.SourceManager
-import eu.kanade.tachiyomi.data.source.model.MangasPage
+import eu.kanade.tachiyomi.data.source.model.FilterList
+import eu.kanade.tachiyomi.data.source.model.SManga
 import eu.kanade.tachiyomi.data.source.online.LoginSource
-import eu.kanade.tachiyomi.data.source.online.OnlineSource
-import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 import rx.Observable
 import rx.Subscription
@@ -55,7 +55,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
     /**
      * Active source.
      */
-    lateinit var source: OnlineSource
+    lateinit var source: CatalogueSource
         private set
 
     /**
@@ -67,12 +67,12 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
     /**
      * Modifiable list of filters.
      */
-    var sourceFilters: List<Filter<*>> = emptyList()
+    var sourceFilters = FilterList()
 
     /**
      * List of filters used by the [Pager]. If empty alongside [query], the popular query is used.
      */
-    var appliedFilters: List<Filter<*>> = emptyList()
+    var appliedFilters = FilterList()
 
     /**
      * Pager containing a list of manga results.
@@ -136,7 +136,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
      * @param query the query.
      * @param filters the current state of the filters (for search mode).
      */
-    fun restartPager(query: String = this.query, filters: List<Filter<*>> = this.appliedFilters) {
+    fun restartPager(query: String = this.query, filters: FilterList = this.appliedFilters) {
         this.query = query
         this.appliedFilters = filters
 
@@ -145,11 +145,17 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
         // Create a new pager.
         pager = createPager(query, filters)
 
+        val sourceId = source.id
+
         // Prepare the pager.
         pagerSubscription?.let { remove(it) }
         pagerSubscription = pager.results()
-                .subscribeReplay({ view, page ->
-                    view.onAddPage(page.page, page.mangas)
+                .observeOn(Schedulers.io())
+                .map { it.first to it.second.map { networkToLocalManga(it, sourceId) } }
+                .doOnNext { initializeMangas(it.second) }
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribeReplay({ view, pair ->
+                    view.onAddPage(pair.first, pair.second)
                 }, { view, error ->
                     Timber.e(error)
                 })
@@ -165,7 +171,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
         if (!hasNextPage()) return
 
         pageSubscription?.let { remove(it) }
-        pageSubscription = pager.requestNext { getPageTransformer(it) }
+        pageSubscription = Observable.defer { pager.requestNext() }
                 .subscribeFirst({ view, page ->
                     // Nothing to do when onNext is emitted.
                 }, CatalogueFragment::onAddPageError)
@@ -175,7 +181,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
      * Returns true if the last fetched page has a next page.
      */
     fun hasNextPage(): Boolean {
-        return pager.hasNextPage()
+        return pager.hasNextPage
     }
 
     /**
@@ -183,12 +189,12 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
      *
      * @param source the new active source.
      */
-    fun setActiveSource(source: OnlineSource) {
+    fun setActiveSource(source: CatalogueSource) {
         prefs.lastUsedCatalogueSource().set(source.id)
         this.source = source
         sourceFilters = source.getFilterList()
 
-        restartPager(query = "", filters = emptyList())
+        restartPager(query = "", filters = FilterList())
     }
 
     /**
@@ -208,7 +214,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
         initializerSubscription?.let { remove(it) }
         initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io())
                 .flatMap { Observable.from(it) }
-                .filter { !it.initialized }
+                .filter { it.thumbnail_url == null && !it.initialized }
                 .concatMap { getMangaDetailsObservable(it) }
                 .onBackpressureBuffer()
                 .observeOn(AndroidSchedulers.mainThread())
@@ -221,41 +227,21 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
                 .apply { add(this) }
     }
 
-    /**
-     * Returns the function to apply to the observable of the list of manga from the source.
-     *
-     * @param observable the observable from the source.
-     * @return the function to apply.
-     */
-    fun getPageTransformer(observable: Observable<MangasPage>): Observable<MangasPage> {
-        return observable.subscribeOn(Schedulers.io())
-                .doOnNext { it.mangas.replace { networkToLocalManga(it) } }
-                .doOnNext { initializeMangas(it.mangas) }
-                .observeOn(AndroidSchedulers.mainThread())
-    }
-
-    /**
-     * Replaces an object in the list with another.
-     */
-    fun <T> MutableList<T>.replace(block: (T) -> T) {
-        forEachIndexed { i, obj ->
-            set(i, block(obj))
-        }
-    }
-
     /**
      * Returns a manga from the database for the given manga from network. It creates a new entry
      * if the manga is not yet in the database.
      *
-     * @param networkManga the manga from network.
+     * @param sManga the manga from the source.
      * @return a manga from the database.
      */
-    private fun networkToLocalManga(networkManga: Manga): Manga {
-        var localManga = db.getManga(networkManga.url, source.id).executeAsBlocking()
+    private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
+        var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
         if (localManga == null) {
-            val result = db.insertManga(networkManga).executeAsBlocking()
-            networkManga.id = result.insertedId()
-            localManga = networkManga
+            val newManga = Manga.create(sManga.url, sManga.title, sourceId)
+            newManga.copyFrom(sManga)
+            val result = db.insertManga(newManga).executeAsBlocking()
+            newManga.id = result.insertedId()
+            localManga = newManga
         }
         return localManga
     }
@@ -279,6 +265,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
         return source.fetchMangaDetails(manga)
                 .flatMap { networkManga ->
                     manga.copyFrom(networkManga)
+                    manga.initialized = true
                     db.insertManga(manga).executeAsBlocking()
                     Observable.just(manga)
                 }
@@ -290,13 +277,13 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
      *
      * @return a source.
      */
-    fun getLastUsedSource(): OnlineSource {
+    fun getLastUsedSource(): CatalogueSource {
         val id = prefs.lastUsedCatalogueSource().get() ?: -1
         val source = sourceManager.get(id)
         if (!isValidSource(source)) {
             return findFirstValidSource()
         }
-        return source as OnlineSource
+        return source as CatalogueSource
     }
 
     /**
@@ -320,14 +307,14 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
      *
      * @return the index of the first valid source.
      */
-    fun findFirstValidSource(): OnlineSource {
+    fun findFirstValidSource(): CatalogueSource {
         return sources.first { isValidSource(it) }
     }
 
     /**
      * Returns a list of enabled sources ordered by language and name.
      */
-    open protected fun getEnabledSources(): List<OnlineSource> {
+    open protected fun getEnabledSources(): List<CatalogueSource> {
         val languages = prefs.enabledLanguages().getOrDefault()
         val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault()
 
@@ -336,7 +323,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
             languages.add("en")
         }
 
-        return sourceManager.getOnlineSources()
+        return sourceManager.getCatalogueSources()
                 .filter { it.lang in languages }
                 .filterNot { it.id.toString() in hiddenCatalogues }
                 .sortedBy { "(${it.lang}) ${it.name}" }
@@ -365,13 +352,13 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
     /**
      * Set the filter states for the current source.
      *
-     * @param filterStates a list of active filters.
+     * @param filters a list of active filters.
      */
-    fun setSourceFilter(filters: List<Filter<*>>) {
+    fun setSourceFilter(filters: FilterList) {
         restartPager(filters = filters)
     }
 
-    open fun createPager(query: String, filters: List<Filter<*>>): Pager {
+    open fun createPager(query: String, filters: FilterList): Pager {
         return CataloguePager(source, query, filters)
     }
 

+ 3 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/NoResultsException.kt

@@ -0,0 +1,3 @@
+package eu.kanade.tachiyomi.ui.catalogue
+
+class NoResultsException : Exception()

+ 14 - 8
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/Pager.kt

@@ -1,25 +1,31 @@
 package eu.kanade.tachiyomi.ui.catalogue
 
+import com.jakewharton.rxrelay.PublishRelay
 import eu.kanade.tachiyomi.data.source.model.MangasPage
-import rx.subjects.PublishSubject
+import eu.kanade.tachiyomi.data.source.model.SManga
 import rx.Observable
 
 /**
  * A general pager for source requests (latest updates, popular, search)
  */
-abstract class Pager {
+abstract class Pager(var currentPage: Int = 1) {
 
-    protected var lastPage: MangasPage? = null
+    var hasNextPage = true
+        private set
 
-    protected val results = PublishSubject.create<MangasPage>()
+    protected val results: PublishRelay<Pair<Int, List<SManga>>> = PublishRelay.create()
 
-    fun results(): Observable<MangasPage> {
+    fun results(): Observable<Pair<Int, List<SManga>>> {
         return results.asObservable()
     }
 
-    fun hasNextPage(): Boolean {
-        return lastPage == null || lastPage?.nextPageUrl != null
+    abstract fun requestNext(): Observable<MangasPage>
+
+    fun onPageReceived(mangasPage: MangasPage) {
+        val page = currentPage
+        currentPage++
+        hasNextPage = mangasPage.hasNextPage && !mangasPage.mangas.isEmpty()
+        results.call(Pair(page, mangasPage.mangas))
     }
 
-    abstract fun requestNext(transformer: (Observable<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage>
 }

+ 9 - 15
app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPager.kt

@@ -1,28 +1,22 @@
 package eu.kanade.tachiyomi.ui.latest_updates
 
+import eu.kanade.tachiyomi.data.source.CatalogueSource
 import eu.kanade.tachiyomi.data.source.model.MangasPage
-import eu.kanade.tachiyomi.data.source.online.OnlineSource
 import eu.kanade.tachiyomi.ui.catalogue.Pager
 import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
 
 /**
  * LatestUpdatesPager inherited from the general Pager.
  */
-class LatestUpdatesPager(val source: OnlineSource): Pager() {
+class LatestUpdatesPager(val source: CatalogueSource): Pager() {
 
-    override fun requestNext(transformer: (Observable<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage> {
-        val lastPage = lastPage
-
-        val page = if (lastPage == null)
-            MangasPage(1)
-        else
-            MangasPage(lastPage.page + 1).apply { url = lastPage.nextPageUrl!! }
-
-        val observable = source.fetchLatestUpdates(page)
-
-        return transformer(observable)
-                .doOnNext { results.onNext(it) }
-                .doOnNext { [email protected] = it }
+    override fun requestNext(): Observable<MangasPage> {
+        return source.fetchLatestUpdates(currentPage)
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .doOnNext { onPageReceived(it) }
     }
 
 }

+ 5 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt

@@ -1,26 +1,26 @@
 package eu.kanade.tachiyomi.ui.latest_updates
 
+import eu.kanade.tachiyomi.data.source.CatalogueSource
 import eu.kanade.tachiyomi.data.source.Source
-import eu.kanade.tachiyomi.data.source.online.OnlineSource
+import eu.kanade.tachiyomi.data.source.model.FilterList
 import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
 import eu.kanade.tachiyomi.ui.catalogue.Pager
-import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
 
 /**
  * Presenter of [LatestUpdatesFragment]. Inherit CataloguePresenter.
  */
 class LatestUpdatesPresenter : CataloguePresenter() {
 
-    override fun createPager(query: String, filters: List<Filter<*>>): Pager {
+    override fun createPager(query: String, filters: FilterList): Pager {
         return LatestUpdatesPager(source)
     }
 
-    override fun getEnabledSources(): List<OnlineSource> {
+    override fun getEnabledSources(): List<CatalogueSource> {
         return super.getEnabledSources().filter { it.supportsLatest }
     }
 
     override fun isValidSource(source: Source?): Boolean {
-        return super.isValidSource(source) && (source as OnlineSource).supportsLatest
+        return super.isValidSource(source) && (source as CatalogueSource).supportsLatest
     }
 
 }

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt

@@ -125,7 +125,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
      */
     private fun applyFilters(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
         // Cached list of downloaded manga directories given a source id.
-        val mangaDirectories = mutableMapOf<Int, Array<UniFile>>()
+        val mangaDirectories = mutableMapOf<Long, Array<UniFile>>()
 
         // Cached list of downloaded chapter directories for a manga.
         val chapterDirectories = mutableMapOf<Long, Boolean>()

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt

@@ -197,7 +197,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
     /**
      * Returns an observable that updates the chapter list with the latest from the source.
      */
-    fun getRemoteChaptersObservable() = source.fetchChapterList(manga)
+    fun getRemoteChaptersObservable() = Observable.defer { source.fetchChapterList(manga) }
             .subscribeOn(Schedulers.io())
             .map { syncChaptersWithSource(db, it, manga, source) }
             .observeOn(AndroidSchedulers.mainThread())

+ 4 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt

@@ -15,6 +15,7 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.source.Source
+import eu.kanade.tachiyomi.data.source.model.SManga
 import eu.kanade.tachiyomi.data.source.online.OnlineSource
 import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
 import eu.kanade.tachiyomi.ui.manga.MangaActivity
@@ -122,9 +123,9 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
 
         // Update status TextView.
         manga_status.setText(when (manga.status) {
-            Manga.ONGOING -> R.string.ongoing
-            Manga.COMPLETED -> R.string.completed
-            Manga.LICENSED -> R.string.licensed
+            SManga.ONGOING -> R.string.ongoing
+            SManga.COMPLETED -> R.string.completed
+            SManga.LICENSED -> R.string.licensed
             else -> R.string.unknown
         })
 

+ 2 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt

@@ -99,9 +99,10 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
      * @return manga information.
      */
     private fun fetchMangaObs(): Observable<Manga> {
-        return source.fetchMangaDetails(manga)
+        return Observable.defer { source.fetchMangaDetails(manga) }
                 .flatMap { networkManga ->
                     manga.copyFrom(networkManga)
+                    manga.initialized = true
                     db.insertManga(manga).executeAsBlocking()
                     Observable.just<Manga>(manga)
                 }

+ 14 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ChapterLoader.kt

@@ -4,6 +4,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.download.DownloadManager
 import eu.kanade.tachiyomi.data.source.Source
 import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.data.source.online.OnlineSource
+import eu.kanade.tachiyomi.data.source.online.fetchImageFromCacheThenNet
+import eu.kanade.tachiyomi.data.source.online.fetchPageListFromCacheThenNet
 import eu.kanade.tachiyomi.util.plusAssign
 import rx.Observable
 import rx.schedulers.Schedulers
@@ -36,9 +39,11 @@ class ChapterLoader(
     }
 
     private fun prepareOnlineReading() {
+        if (source !is OnlineSource) return
+
         subscriptions += Observable.defer { Observable.just(queue.take().page) }
                 .filter { it.status == Page.QUEUE }
-                .concatMap { source.fetchImage(it) }
+                .concatMap { source.fetchImageFromCacheThenNet(it) }
                 .repeat()
                 .subscribeOn(Schedulers.io())
                 .subscribe({
@@ -57,6 +62,10 @@ class ChapterLoader(
                     Observable.just(chapter.pages!!)
             }
             .doOnNext { pages ->
+                if (pages.isEmpty()) {
+                    throw Exception("Page list is empty")
+                }
+
                 // Now that the number of pages is known, fix the requested page if the last one
                 // was requested.
                 if (chapter.requestedPage == -1) {
@@ -76,8 +85,8 @@ class ChapterLoader(
                     // Fetch the page list from disk.
                     downloadManager.buildPageList(source, manga, chapter)
                 } else {
-                    // Fetch the page list from cache or fallback to network
-                    source.fetchPageList(chapter)
+                    (source as? OnlineSource)?.fetchPageListFromCacheThenNet(chapter)
+                            ?: source.fetchPageList(chapter)
                 }
             }
             .doOnNext { pages ->
@@ -111,6 +120,8 @@ class ChapterLoader(
         queue.offer(PriorityPage(page, 2))
     }
 
+
+
     private data class PriorityPage(val page: Page, val priority: Int): Comparable<PriorityPage> {
 
         companion object {

+ 3 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt

@@ -372,7 +372,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
         Observable.fromCallable {
             // Cache current page list progress for online chapters to allow a faster reopen
             if (!chapter.isDownloaded) {
-                source.let { if (it is OnlineSource) it.savePageList(chapter, pages) }
+                source.let {
+                    if (it is OnlineSource) chapterCache.putPageListToCache(chapter, pages)
+                }
             }
 
             try {

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt

@@ -130,7 +130,7 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
      */
     private fun setDownloadedChapters(chapters: List<RecentChapter>) {
         // Cached list of downloaded manga directories.
-        val mangaDirectories = mutableMapOf<Int, Array<UniFile>>()
+        val mangaDirectories = mutableMapOf<Long, Array<UniFile>>()
 
         // Cached list of downloaded chapter directories for a manga.
         val chapterDirectories = mutableMapOf<Long, Array<UniFile>>()

+ 4 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesFragment.kt

@@ -123,13 +123,14 @@ class SettingsSourcesFragment : SettingsFragment() {
     }
 
     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
-        if (requestCode == SOURCE_CHANGE_REQUEST) {
-            val pref = findPreference(getSourceKey(resultCode)) as? LoginCheckBoxPreference
+        if (requestCode == SOURCE_CHANGE_REQUEST && data != null) {
+            val sourceId = data.getLongExtra("key", -1L)
+            val pref = findPreference(getSourceKey(sourceId)) as? LoginCheckBoxPreference
             pref?.notifyChanged()
         }
     }
 
-    private fun getSourceKey(sourceId: Int): String {
+    private fun getSourceKey(sourceId: Long): String {
         return "source_$sourceId"
     }
 

+ 3 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingFragment.kt

@@ -81,8 +81,9 @@ class SettingsTrackingFragment : SettingsFragment() {
     }
 
     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
-        if (requestCode == SYNC_CHANGE_REQUEST) {
-            updatePreference(resultCode)
+        if (requestCode == SYNC_CHANGE_REQUEST && data != null) {
+            val serviceId = data.getIntExtra("key", -1)
+            updatePreference(serviceId)
         }
     }
 

+ 13 - 6
app/src/main/java/eu/kanade/tachiyomi/util/ChapterSourceSync.kt

@@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
 import eu.kanade.tachiyomi.data.database.models.Chapter
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.source.Source
+import eu.kanade.tachiyomi.data.source.model.SChapter
 import eu.kanade.tachiyomi.data.source.online.OnlineSource
 import java.util.*
 
@@ -11,23 +12,29 @@ import java.util.*
  * Helper method for syncing the list of chapters from the source with the ones from the database.
  *
  * @param db the database.
- * @param sourceChapters a list of chapters from the source.
+ * @param rawSourceChapters a list of chapters from the source.
  * @param manga the manga of the chapters.
  * @param source the source of the chapters.
  * @return a pair of new insertions and deletions.
  */
 fun syncChaptersWithSource(db: DatabaseHelper,
-                           sourceChapters: List<Chapter>,
+                           rawSourceChapters: List<SChapter>,
                            manga: Manga,
                            source: Source) : Pair<List<Chapter>, List<Chapter>> {
 
+    if (rawSourceChapters.isEmpty()) {
+        throw Exception("No chapters found")
+    }
+
     // Chapters from db.
     val dbChapters = db.getChapters(manga).executeAsBlocking()
 
-    // Fix manga id and order in source.
-    sourceChapters.forEachIndexed { i, chapter ->
-        chapter.manga_id = manga.id
-        chapter.source_order = i
+    val sourceChapters = rawSourceChapters.mapIndexed { i, sChapter ->
+        Chapter.create().apply {
+            copyFrom(sChapter)
+            manga_id = manga.id
+            source_order = i
+        }
     }
 
     // Chapters from the source not in db.

+ 0 - 26
app/src/main/java/eu/kanade/tachiyomi/util/UrlUtil.java

@@ -1,26 +0,0 @@
-package eu.kanade.tachiyomi.util;
-
-import java.net.URI;
-import java.net.URISyntaxException;
-
-public final class UrlUtil {
-
-    private UrlUtil() throws InstantiationException {
-        throw new InstantiationException("This class is not for instantiation");
-    }
-
-    public static String getPath(String s) {
-        try {
-            URI uri = new URI(s);
-            String out = uri.getPath();
-            if (uri.getQuery() != null)
-                out += "?" + uri.getQuery();
-            if (uri.getFragment() != null)
-                out += "#" + uri.getFragment();
-            return out;
-        } catch (URISyntaxException e) {
-            return s;
-        }
-    }
-
-}

+ 2 - 1
app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt

@@ -21,10 +21,11 @@ fun View.getCoordinates() = Point((left + right) / 2, (top + bottom) / 2)
  * @param length the duration of the snack.
  * @param f a function to execute in the snack, allowing for example to define a custom action.
  */
-inline fun View.snack(message: String, length: Int = Snackbar.LENGTH_LONG, f: Snackbar.() -> Unit) {
+inline fun View.snack(message: String, length: Int = Snackbar.LENGTH_LONG, f: Snackbar.() -> Unit): Snackbar {
     val snack = Snackbar.make(this, message, length)
     val textView = snack.view.findViewById(android.support.design.R.id.snackbar_text) as TextView
     textView.setTextColor(Color.WHITE)
     snack.f()
     snack.show()
+    return snack
 }

+ 3 - 1
app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt

@@ -1,5 +1,6 @@
 package eu.kanade.tachiyomi.widget.preference
 
+import android.app.Activity
 import android.app.Dialog
 import android.content.DialogInterface
 import android.content.Intent
@@ -70,7 +71,8 @@ abstract class LoginDialogPreference : DialogFragment() {
 
     override fun onDismiss(dialog: DialogInterface) {
         super.onDismiss(dialog)
-        targetFragment?.onActivityResult(targetRequestCode, arguments.getInt("key"), Intent())
+        val intent = Intent().putExtras(arguments)
+        targetFragment?.onActivityResult(targetRequestCode, Activity.RESULT_OK, intent)
     }
 
     protected abstract fun checkLogin()

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/widget/preference/SourceLoginDialog.kt

@@ -19,7 +19,7 @@ class SourceLoginDialog : LoginDialogPreference() {
         fun newInstance(source: Source): LoginDialogPreference {
             val fragment = SourceLoginDialog()
             val bundle = Bundle(1)
-            bundle.putInt("key", source.id)
+            bundle.putLong("key", source.id)
             fragment.arguments = bundle
             return fragment
         }
@@ -32,7 +32,7 @@ class SourceLoginDialog : LoginDialogPreference() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
-        val sourceId = arguments.getInt("key")
+        val sourceId = arguments.getLong("key")
         source = sourceManager.get(sourceId) as LoginSource
     }
 

+ 1 - 1
app/src/main/res/values/keys.xml

@@ -64,7 +64,7 @@
     <string name="pref_enable_automatic_updates_key">automatic_updates</string>
 
     <string name="pref_display_catalogue_as_list">pref_display_catalogue_as_list</string>
-    <string name="pref_last_catalogue_source_key">pref_last_catalogue_source_key</string>
+    <string name="pref_last_catalogue_source_key">last_catalogue_source</string>
 
     <string name="pref_download_new_key">download_new</string>
 

+ 5 - 5
app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.kt

@@ -9,12 +9,13 @@ import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner
 import eu.kanade.tachiyomi.data.database.models.Chapter
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.source.SourceManager
+import eu.kanade.tachiyomi.data.source.model.SChapter
 import eu.kanade.tachiyomi.data.source.online.OnlineSource
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Matchers.anyInt
+import org.mockito.Matchers.anyLong
 import org.mockito.Mockito
 import org.mockito.Mockito.*
 import org.robolectric.Robolectric
@@ -51,7 +52,7 @@ class LibraryUpdateServiceTest {
 
         service = Robolectric.setupService(LibraryUpdateService::class.java)
         source = mock(OnlineSource::class.java)
-        `when`(service.sourceManager.get(anyInt())).thenReturn(source)
+        `when`(service.sourceManager.get(anyLong())).thenReturn(source)
     }
 
     @Test
@@ -91,7 +92,7 @@ class LibraryUpdateServiceTest {
 
         // One of the updates will fail
         `when`(source.fetchChapterList(favManga[0])).thenReturn(Observable.just(chapters))
-        `when`(source.fetchChapterList(favManga[1])).thenReturn(Observable.error<List<Chapter>>(Exception()))
+        `when`(source.fetchChapterList(favManga[1])).thenReturn(Observable.error<List<SChapter>>(Exception()))
         `when`(source.fetchChapterList(favManga[2])).thenReturn(Observable.just(chapters3))
 
         val intent = Intent()
@@ -117,8 +118,7 @@ class LibraryUpdateServiceTest {
     private fun createManga(vararg urls: String): List<Manga> {
         val list = ArrayList<Manga>()
         for (url in urls) {
-            val m = Manga.create(url)
-            m.title = url.substring(1)
+            val m = Manga.create(url, url.substring(1))
             m.favorite = true
             list.add(m)
         }