Эх сурвалжийг харах

Initial support for external sources

inorichi 8 жил өмнө
parent
commit
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.github.salomonbrys.kotson.fromJson
 import com.google.gson.Gson
 import com.google.gson.Gson
 import com.jakewharton.disklrucache.DiskLruCache
 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.data.source.model.Page
 import eu.kanade.tachiyomi.util.DiskUtil
 import eu.kanade.tachiyomi.util.DiskUtil
 import eu.kanade.tachiyomi.util.saveTo
 import eu.kanade.tachiyomi.util.saveTo
@@ -92,13 +93,13 @@ class ChapterCache(private val context: Context) {
     /**
     /**
      * Get page list from cache.
      * Get page list from cache.
      *
      *
-     * @param chapterUrl the url of the chapter.
+     * @param chapter the chapter.
      * @return an observable of the list of pages.
      * @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.
             // 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
             // Convert JSON string to list of objects. Throws an exception if snapshot is null
             diskCache.get(key).use {
             diskCache.get(key).use {
@@ -110,10 +111,10 @@ class ChapterCache(private val context: Context) {
     /**
     /**
      * Add page list to disk cache.
      * Add page list to disk cache.
      *
      *
-     * @param chapterUrl the url of the chapter.
+     * @param chapter the chapter.
      * @param pages list of pages.
      * @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.
         // Convert list of pages to json string.
         val cachedValue = gson.toJson(pages)
         val cachedValue = gson.toJson(pages)
 
 
@@ -122,7 +123,7 @@ class ChapterCache(private val context: Context) {
 
 
         try {
         try {
             // Get editor from md5 key.
             // Get editor from md5 key.
-            val key = DiskUtil.hashKeyForDisk(chapterUrl)
+            val key = DiskUtil.hashKeyForDisk(getKey(chapter))
             editor = diskCache.edit(key) ?: return
             editor = diskCache.edit(key) ?: return
 
 
             // Write chapter urls to cache.
             // 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 {
     override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply {
         id = cursor.getLong(cursor.getColumnIndex(COL_ID))
         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))
         url = cursor.getString(cursor.getColumnIndex(COL_URL))
         artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
         artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
         author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR))
         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
 package eu.kanade.tachiyomi.data.database.models
 
 
+import eu.kanade.tachiyomi.data.source.model.SChapter
 import java.io.Serializable
 import java.io.Serializable
 
 
-interface Chapter : Serializable {
+interface Chapter : SChapter, Serializable {
 
 
     var id: Long?
     var id: Long?
 
 
     var manga_id: Long?
     var manga_id: Long?
 
 
-    var url: String
-
-    var name: String
-
     var read: Boolean
     var read: Boolean
 
 
     var bookmark: Boolean
     var bookmark: Boolean
@@ -20,10 +17,6 @@ interface Chapter : Serializable {
 
 
     var date_fetch: Long
     var date_fetch: Long
 
 
-    var date_upload: Long
-
-    var chapter_number: Float
-
     var source_order: Int
     var source_order: Int
 
 
     val isRecognizedNumber: Boolean
     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
 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 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 favorite: Boolean
 
 
     var last_update: Long
     var last_update: Long
 
 
-    var initialized: Boolean
-
     var viewer: Int
     var viewer: Int
 
 
     var chapter_flags: Int
     var chapter_flags: Int
@@ -38,27 +20,6 @@ interface Manga : Serializable {
 
 
     var category: Int
     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) {
     fun setChapterOrder(order: Int) {
         setFlags(order, SORT_MASK)
         setFlags(order, SORT_MASK)
     }
     }
@@ -94,11 +55,6 @@ interface Manga : Serializable {
 
 
     companion object {
     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_DESC = 0x00000000
         const val SORT_ASC = 0x00000001
         const val SORT_ASC = 0x00000001
         const val SORT_MASK = 0x00000001
         const val SORT_MASK = 0x00000001
@@ -126,12 +82,13 @@ interface Manga : Serializable {
         const val DISPLAY_NUMBER = 0x00100000
         const val DISPLAY_NUMBER = 0x00100000
         const val DISPLAY_MASK = 0x00100000
         const val DISPLAY_MASK = 0x00100000
 
 
-        fun create(source: Int): Manga = MangaImpl().apply {
+        fun create(source: Long): Manga = MangaImpl().apply {
             this.source = source
             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
             url = pathUrl
+            this.title = title
             this.source = source
             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 id: Long? = null
 
 
-    override var source: Int = 0
+    override var source: Long = 0
 
 
     override lateinit var url: String
     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())
                     .build())
             .prepare()
             .prepare()
 
 
-    fun getManga(url: String, sourceId: Int) = db.get()
+    fun getManga(url: String, sourceId: Long) = db.get()
             .`object`(Manga::class.java)
             .`object`(Manga::class.java)
             .withQuery(Query.builder()
             .withQuery(Query.builder()
                     .table(MangaTable.TABLE)
                     .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.SourceManager
 import eu.kanade.tachiyomi.data.source.model.Page
 import eu.kanade.tachiyomi.data.source.model.Page
 import eu.kanade.tachiyomi.data.source.online.OnlineSource
 import eu.kanade.tachiyomi.data.source.online.OnlineSource
+import eu.kanade.tachiyomi.data.source.online.fetchAllImageUrlsFromPageList
 import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
 import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
 import eu.kanade.tachiyomi.util.RetryWithDelay
 import eu.kanade.tachiyomi.util.RetryWithDelay
 import eu.kanade.tachiyomi.util.plusAssign
 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) {
         val pageListObservable = if (download.pages == null) {
             // Pull page list from network and add them to download object
             // Pull page list from network and add them to download object
-            download.source.fetchPageListFromNetwork(download.chapter)
+            download.source.fetchPageList(download.chapter)
                     .doOnNext { pages ->
                     .doOnNext { pages ->
+                        if (pages.isEmpty()) {
+                            throw Exception("Page list is empty")
+                        }
                         download.pages = pages
                         download.pages = pages
                     }
                     }
         } else {
         } 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> {
     private fun downloadImage(page: Page, source: OnlineSource, tmpDir: UniFile, filename: String): Observable<UniFile> {
         page.status = Page.DOWNLOAD_IMAGE
         page.status = Page.DOWNLOAD_IMAGE
         page.progress = 0
         page.progress = 0
-        return source.imageResponse(page)
+        return source.fetchImage(page)
                 .map { response ->
                 .map { response ->
                     val file = tmpDir.createFile("$filename.tmp")
                     val file = tmpDir.createFile("$filename.tmp")
                     try {
                     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.
      * 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.
      * 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.PreferencesHelper
 import eu.kanade.tachiyomi.data.preference.getOrDefault
 import eu.kanade.tachiyomi.data.preference.getOrDefault
 import eu.kanade.tachiyomi.data.source.SourceManager
 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.data.source.online.OnlineSource
 import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.util.*
 import eu.kanade.tachiyomi.util.*
@@ -214,7 +215,7 @@ class LibraryUpdateService : Service() {
         }
         }
 
 
         if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) {
         if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) {
-            listToUpdate = listToUpdate.filter { it.status != Manga.COMPLETED }
+            listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
         }
         }
 
 
         return listToUpdate
         return listToUpdate
@@ -328,9 +329,10 @@ class LibraryUpdateService : Service() {
                             ?: return@concatMap Observable.empty<Manga>()
                             ?: return@concatMap Observable.empty<Manga>()
 
 
                     source.fetchMangaDetails(manga)
                     source.fetchMangaDetails(manga)
-                            .doOnNext { networkManga ->
+                            .map { networkManga ->
                                 manga.copyFrom(networkManga)
                                 manga.copyFrom(networkManga)
                                 db.insertManga(manga).executeAsBlocking()
                                 db.insertManga(manga).executeAsBlocking()
+                                manga
                             }
                             }
                             .onErrorReturn { 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)
     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"
     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 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)
     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
 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.Page
+import eu.kanade.tachiyomi.data.source.model.SChapter
+import eu.kanade.tachiyomi.data.source.model.SManga
 import rx.Observable
 import rx.Observable
 
 
 /**
 /**
@@ -13,7 +13,7 @@ interface Source {
     /**
     /**
      * Id for the source. Must be unique.
      * Id for the source. Must be unique.
      */
      */
-    val id: Int
+    val id: Long
 
 
     /**
     /**
      * Name of the source.
      * Name of the source.
@@ -25,26 +25,20 @@ interface Source {
      *
      *
      * @param manga the manga to update.
      * @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.
      * Returns an observable with all the available chapters for a manga.
      *
      *
      * @param manga the manga to update.
      * @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.
      * Returns an observable with the list of pages a chapter has.
      *
      *
      * @param chapter the chapter.
      * @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.Manifest.permission.READ_EXTERNAL_STORAGE
 import android.content.Context
 import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
 import android.os.Environment
 import android.os.Environment
+import dalvik.system.PathClassLoader
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.source.online.OnlineSource
 import eu.kanade.tachiyomi.data.source.online.OnlineSource
 import eu.kanade.tachiyomi.data.source.online.YamlOnlineSource
 import eu.kanade.tachiyomi.data.source.online.YamlOnlineSource
@@ -18,29 +21,47 @@ import java.io.File
 
 
 open class SourceManager(private val context: Context) {
 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]
         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 +
         val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
                 File.separator + context.getString(R.string.app_name), "parsers")
                 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" }) {
             for (file in parsersDir.listFiles().filter { it.extension == "yml" }) {
                 try {
                 try {
                     val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
                     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) {
                 } catch (e: Exception) {
                     Timber.e("Error loading source from file. Bad format?")
                     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
 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
 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.GET
 import eu.kanade.tachiyomi.data.network.NetworkHelper
 import eu.kanade.tachiyomi.data.network.NetworkHelper
 import eu.kanade.tachiyomi.data.network.asObservableSuccess
 import eu.kanade.tachiyomi.data.network.asObservableSuccess
 import eu.kanade.tachiyomi.data.network.newCallWithProgress
 import eu.kanade.tachiyomi.data.network.newCallWithProgress
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 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.Headers
 import okhttp3.OkHttpClient
 import okhttp3.OkHttpClient
 import okhttp3.Request
 import okhttp3.Request
 import okhttp3.Response
 import okhttp3.Response
 import rx.Observable
 import rx.Observable
 import uy.kohesive.injekt.injectLazy
 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.
  * A simple implementation for sources from a website.
  */
  */
-abstract class OnlineSource() : Source {
+abstract class OnlineSource : CatalogueSource {
 
 
     /**
     /**
      * Network service.
      * Network service.
      */
      */
     val network: NetworkHelper by injectLazy()
     val network: NetworkHelper by injectLazy()
 
 
-    /**
-     * Chapter cache.
-     */
-    val chapterCache: ChapterCache by injectLazy()
-
     /**
     /**
      * Preferences helper.
      * Preferences helper.
      */
      */
@@ -46,24 +38,26 @@ abstract class OnlineSource() : Source {
     abstract val baseUrl: String
     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.
      * 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.
      * 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
      * Returns an observable containing a page with a list of manga. Normally it's not needed to
      * override this method.
      * 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 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
      * Returns an observable containing a page with a list of manga. Normally it's not needed to
      * override this method.
      * 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 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 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 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
      * 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.
      * @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.
      * @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)
         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 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
      * 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.
      * @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
      * 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.
      * @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)
         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 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.
      * @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
      * 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.
      * 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)
         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 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
      * 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.
      * @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()
                 .asObservableSuccess()
                 .map { imageUrlParse(it) }
                 .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.
      * @param response the response from the site.
      */
      */
     abstract protected fun imageUrlParse(response: Response): String
     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.
      * Returns an observable with the response of the source image.
      *
      *
      * @param page the page whose source image has to be downloaded.
      * @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
      * 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
      * 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].
      * 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 chapter the chapter to be added.
      * @param manga the manga of the chapter.
      * @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
 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.MangasPage
 import eu.kanade.tachiyomi.data.source.model.Page
 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 eu.kanade.tachiyomi.util.asJsoup
 import okhttp3.Response
 import okhttp3.Response
 import org.jsoup.nodes.Document
 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.
  * 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 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()
         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
     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 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
      * 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?
     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 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()
         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
     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 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
      * 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?
     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()
         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
     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?
     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 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 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 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()
         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
     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 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 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 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.
      * 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
 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.GET
 import eu.kanade.tachiyomi.data.network.POST
 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.asJsoup
 import eu.kanade.tachiyomi.util.attrOrText
 import eu.kanade.tachiyomi.util.attrOrText
 import okhttp3.Request
 import okhttp3.Request
@@ -36,92 +33,108 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
     }
     }
 
 
     override val id = map.id.let {
     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()) {
         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()
         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()
                 title = element.text()
                 setUrlWithoutDomain(element.attr("href"))
                 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()) {
         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()
         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()
                 title = element.text()
                 setUrlWithoutDomain(element.attr("href"))
                 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()) {
         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()
         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()
                 title = element.text()
                 setUrlWithoutDomain(element.attr("href"))
                 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 document = response.asJsoup()
+
+        val manga = SManga.create()
         with(map.manga) {
         with(map.manga) {
             val pool = parts.get(document)
             val pool = parts.get(document)
 
 
@@ -130,18 +143,21 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
             manga.description = summary?.process(document, pool)
             manga.description = summary?.process(document, pool)
             manga.thumbnail_url = cover?.process(document, pool)
             manga.thumbnail_url = cover?.process(document, pool)
             manga.genre = genres?.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 document = response.asJsoup()
+
+        val chapters = mutableListOf<SChapter>()
         with(map.chapters) {
         with(map.chapters) {
             val pool = emptyMap<String, Element>()
             val pool = emptyMap<String, Element>()
             val dateFormat = SimpleDateFormat(date?.format, Locale.ENGLISH)
             val dateFormat = SimpleDateFormat(date?.format, Locale.ENGLISH)
 
 
             for (element in document.select(chapter_css)) {
             for (element in document.select(chapter_css)) {
-                val chapter = Chapter.create()
+                val chapter = SChapter.create()
                 element.select(title).first().let {
                 element.select(title).first().let {
                     chapter.name = it.text()
                     chapter.name = it.text()
                     chapter.setUrlWithoutDomain(it.attr("href"))
                     chapter.setUrlWithoutDomain(it.attr("href"))
@@ -151,12 +167,15 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
                 chapters.add(chapter)
                 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 body = response.body().string()
         val url = response.request().url().toString()
         val url = response.request().url().toString()
 
 
+        val pages = mutableListOf<Page>()
+
         // TODO lazy initialization in Kotlin 1.1
         // TODO lazy initialization in Kotlin 1.1
         val document = Jsoup.parse(body, url)
         val document = Jsoup.parse(body, url)
 
 
@@ -194,6 +213,7 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
                 page.imageUrl = url
                 page.imageUrl = url
             }
             }
         }
         }
+        return pages
     }
     }
 
 
     override fun imageUrlParse(response: Response): String {
     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
 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.FormBody
 import okhttp3.RequestBody
 import okhttp3.RequestBody
 import org.jsoup.nodes.Document
 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 {
     fun getStatus(document: Element, cache: Map<String, Element>): Int {
         val text = process(document, cache)
         val text = process(document, cache)
         complete?.let {
         complete?.let {
-            if (text.contains(it)) return Manga.COMPLETED
+            if (text.contains(it)) return SManga.COMPLETED
         }
         }
         ongoing?.let {
         ongoing?.let {
-            if (text.contains(it)) return Manga.ONGOING
+            if (text.contains(it)) return SManga.ONGOING
         }
         }
         licensed?.let {
         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
 package eu.kanade.tachiyomi.data.source.online.english
 
 
 import android.text.Html
 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.GET
 import eu.kanade.tachiyomi.data.network.POST
 import eu.kanade.tachiyomi.data.network.POST
 import eu.kanade.tachiyomi.data.network.asObservable
 import eu.kanade.tachiyomi.data.network.asObservable
-import eu.kanade.tachiyomi.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.LoginSource
 import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
 import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
 import eu.kanade.tachiyomi.util.asJsoup
 import eu.kanade.tachiyomi.util.asJsoup
@@ -25,7 +22,9 @@ import java.text.SimpleDateFormat
 import java.util.*
 import java.util.*
 import java.util.regex.Pattern
 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"
     override val name = "Batoto"
 
 
@@ -56,70 +55,46 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
             .add("Referer", "http://bato.to/reader")
             .add("Referer", "http://bato.to/reader")
             .build()
             .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 popularMangaSelector() = "tr:has(a)"
 
 
     override fun latestUpdatesSelector() = "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 {
         element.select("a[href^=http://bato.to]").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.text().trim()
             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 popularMangaNextPageSelector() = "#show_more_row"
 
 
     override fun latestUpdatesNextPageSelector() = "#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()
         val url = HttpUrl.parse("$baseUrl/search_ajax").newBuilder()
         if (!query.isEmpty()) url.addQueryParameter("name", query).addQueryParameter("name_cond", "c")
         if (!query.isEmpty()) url.addQueryParameter("name", query).addQueryParameter("name_cond", "c")
         var genres = ""
         var genres = ""
-        for (filter in if (filterStates.isEmpty()) filters else filterStates) {
+        filters.forEach { filter ->
             when (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 -> {
                 is TextField -> {
                     if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
                     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)
         if (!genres.isEmpty()) url.addQueryParameter("genres", genres)
         url.addQueryParameter("p", page.toString())
         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 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 searchMangaNextPageSelector() = popularMangaNextPageSelector()
 
 
-    override fun mangaDetailsRequest(manga: Manga): Request {
+    override fun mangaDetailsRequest(manga: SManga): Request {
         val mangaId = manga.url.substringAfterLast("r")
         val mangaId = manga.url.substringAfterLast("r")
         return GET("$baseUrl/comic_pop?id=$mangaId", headers)
         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 tbody = document.select("tbody").first()
         val artistElement = tbody.select("tr:contains(Author/Artist:)").first()
         val artistElement = tbody.select("tr:contains(Author/Artist:)").first()
 
 
+        val manga = SManga.create()
         manga.author = artistElement.selectText("td:eq(1)")
         manga.author = artistElement.selectText("td:eq(1)")
         manga.artist = artistElement.selectText("td:eq(2)") ?: manga.author
         manga.artist = artistElement.selectText("td:eq(2)") ?: manga.author
         manga.description = tbody.selectText("tr:contains(Description:) > td:eq(1)")
         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.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.status = parseStatus(document.selectText("tr:contains(Status:) > td:eq(1)"))
         manga.genre = tbody.select("tr:contains(Genres:) img").map { it.attr("alt") }.joinToString(", ")
         manga.genre = tbody.select("tr:contains(Genres:) img").map { it.attr("alt") }.joinToString(", ")
+        return manga
     }
     }
 
 
     private fun parseStatus(status: String?) = when (status) {
     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 body = response.body().string()
         val matcher = staffNotice.matcher(body)
         val matcher = staffNotice.matcher(body)
         if (matcher.find()) {
         if (matcher.find()) {
+            @Suppress("DEPRECATION")
             val notice = Html.fromHtml(matcher.group(1)).toString().trim()
             val notice = Html.fromHtml(matcher.group(1)).toString().trim()
             throw Exception(notice)
             throw Exception(notice)
         }
         }
 
 
         val document = response.asJsoup(body)
         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 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 urlElement = element.select("a[href^=http://bato.to/reader").first()
 
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.name = urlElement.text()
         chapter.name = urlElement.text()
         chapter.date_upload = element.select("td").getOrNull(4)?.let {
         chapter.date_upload = element.select("td").getOrNull(4)?.let {
             parseDateFromElement(it)
             parseDateFromElement(it)
         } ?: 0
         } ?: 0
+        return chapter
     }
     }
 
 
     private fun parseDateFromElement(dateElement: Element): Long {
     private fun parseDateFromElement(dateElement: Element): Long {
@@ -246,12 +199,13 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
         return date.time
         return date.time
     }
     }
 
 
-    override fun pageListRequest(chapter: Chapter): Request {
+    override fun pageListRequest(chapter: SChapter): Request {
         val id = chapter.url.substringAfterLast("#")
         val id = chapter.url.substringAfterLast("#")
         return GET("$baseUrl/areader?id=$id&p=1", pageHeaders)
         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()
         val selectElement = document.select("#page_select").first()
         if (selectElement != null) {
         if (selectElement != null) {
             for ((i, element) in selectElement.select("option").withIndex()) {
             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")))
                 pages.add(Page(i, "", element.attr("src")))
             }
             }
         }
         }
+        return pages
     }
     }
 
 
     override fun imageUrlRequest(page: Page): Request {
     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" }
         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()) {
         if (!isLogged()) {
             val username = preferences.sourceUsername(this)
             val username = preferences.sourceUsername(this)
             val password = preferences.sourcePassword(this)
             val password = preferences.sourcePassword(this)
@@ -328,7 +283,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
         override fun toString(): String = name
         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 Genre(name: String, val id: Int) : Filter.TriState(name)
     private class TextField(name: String, val key: String) : Filter.Text(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)
     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})`
     //     const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Genre("${el.textContent.trim()}", ${id})`
     // }).join(',\n')
     // }).join(',\n')
     // on https://bato.to/search
     // on https://bato.to/search
-    override fun getFilterList(): List<Filter<*>> = listOf(
+    override fun getFilterList() = FilterList(
             TextField("Author", "artist_name"),
             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"))),
             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(),
             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
 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.GET
 import eu.kanade.tachiyomi.data.network.POST
 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.data.source.online.ParsedOnlineSource
 import okhttp3.FormBody
 import okhttp3.FormBody
 import okhttp3.OkHttpClient
 import okhttp3.OkHttpClient
@@ -16,7 +13,9 @@ import org.jsoup.nodes.Element
 import java.text.SimpleDateFormat
 import java.text.SimpleDateFormat
 import java.util.regex.Pattern
 import java.util.regex.Pattern
 
 
-class Kissmanga(override val id: Int) : ParsedOnlineSource() {
+class Kissmanga : ParsedOnlineSource() {
+
+    override val id: Long = 4
 
 
     override val name = "Kissmanga"
     override val name = "Kissmanga"
 
 
@@ -28,38 +27,40 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
 
 
     override val client: OkHttpClient = network.cloudflareClient
     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 popularMangaSelector() = "table.listing tr:gt(1)"
 
 
     override fun latestUpdatesSelector() = "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 {
         element.select("td a:eq(0)").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.text()
             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 popularMangaNextPageSelector() = "li > a:contains(› Next)"
 
 
     override fun latestUpdatesNextPageSelector(): String = "ul.pager > 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 {
         val form = FormBody.Builder().apply {
             add("mangaName", query)
             add("mangaName", query)
 
 
-            for (filter in if (filters.isEmpty()) [email protected] else filters) {
+            for (filter in if (filters.isEmpty()) getFilterList() else filters) {
                 when (filter) {
                 when (filter) {
                     is Author -> add("authorArtist", filter.state)
                     is Author -> add("authorArtist", filter.state)
                     is Status -> add("status", arrayOf("", "Completed", "Ongoing")[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 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 searchMangaNextPageSelector() = null
 
 
-    override fun mangaDetailsParse(document: Document, manga: Manga) {
+    override fun mangaDetailsParse(document: Document): SManga {
         val infoElement = document.select("div.barContent").first()
         val infoElement = document.select("div.barContent").first()
 
 
+        val manga = SManga.create()
         manga.author = infoElement.select("p:has(span:contains(Author:)) > a").first()?.text()
         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.genre = infoElement.select("p:has(span:contains(Genres:)) > *:gt(0)").text()
         manga.description = infoElement.select("p:has(span:contains(Summary:)) ~ p").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.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")
         manga.thumbnail_url = document.select(".rightBox:eq(0) img").first()?.attr("src")
+        return manga
     }
     }
 
 
     fun parseStatus(status: String) = when {
     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 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 urlElement = element.select("a").first()
 
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.name = urlElement.text()
         chapter.name = urlElement.text()
         chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
         chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
             SimpleDateFormat("MM/dd/yyyy").parse(it).time
             SimpleDateFormat("MM/dd/yyyy").parse(it).time
         } ?: 0
         } ?: 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
         //language=RegExp
         val p = Pattern.compile("""lstImages.push\("(.+?)"""")
         val p = Pattern.compile("""lstImages.push\("(.+?)"""")
         val m = p.matcher(response.body().string())
         val m = p.matcher(response.body().string())
@@ -119,10 +123,11 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
         while (m.find()) {
         while (m.find()) {
             pages.add(Page(i++, "", m.group(1)))
             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)
     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 Status() : Filter.TriState("Completed")
     private class Author() : Filter.Text("Author")
     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')
     // $("select[name=\"genres\"]").map((i,el) => `Genre("${$(el).next().text().trim()}", ${i})`).get().join(',\n')
     // on http://kissmanga.com/AdvanceSearch
     // on http://kissmanga.com/AdvanceSearch
-    override fun getFilterList(): List<Filter<*>> = listOf(
+    override fun getFilterList() = FilterList(
             Author(),
             Author(),
             Status(),
             Status(),
             Filter.Header("Genres"),
             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
 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.data.source.online.ParsedOnlineSource
-import eu.kanade.tachiyomi.util.asJsoup
 import okhttp3.HttpUrl
 import okhttp3.HttpUrl
-import okhttp3.Response
+import okhttp3.Request
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
 import org.jsoup.nodes.Element
 import java.text.ParseException
 import java.text.ParseException
 import java.text.SimpleDateFormat
 import java.text.SimpleDateFormat
 import java.util.*
 import java.util.*
 
 
-class Mangafox(override val id: Int) : ParsedOnlineSource() {
+class Mangafox : ParsedOnlineSource() {
+
+    override val id: Long = 3
 
 
     override val name = "Mangafox"
     override val name = "Mangafox"
 
 
@@ -23,32 +23,40 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
 
 
     override val supportsLatest = true
     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 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 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 {
         element.select("a.title").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.text()
             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 popularMangaNextPageSelector() = "a:has(span.next)"
 
 
     override fun latestUpdatesNextPageSelector() = "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)
         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) {
             when (filter) {
                 is Genre -> url.addQueryParameter(filter.id, filter.state.toString())
                 is Genre -> url.addQueryParameter(filter.id, filter.state.toString())
                 is TextField -> url.addQueryParameter(filter.key, filter.state)
                 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")
                 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 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 {
         element.select("a.title").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.text()
             manga.title = it.text()
         }
         }
+        return manga
     }
     }
 
 
     override fun searchMangaNextPageSelector() = "a:has(span.next)"
     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 infoElement = document.select("div#title").first()
         val rowElement = infoElement.select("table > tbody > tr:eq(1)").first()
         val rowElement = infoElement.select("table > tbody > tr:eq(1)").first()
         val sideInfoElement = document.select("#series_info").first()
         val sideInfoElement = document.select("#series_info").first()
 
 
+        val manga = SManga.create()
         manga.author = rowElement.select("td:eq(1)").first()?.text()
         manga.author = rowElement.select("td:eq(1)").first()?.text()
         manga.artist = rowElement.select("td:eq(2)").first()?.text()
         manga.artist = rowElement.select("td:eq(2)").first()?.text()
         manga.genre = rowElement.select("td:eq(3)").first()?.text()
         manga.genre = rowElement.select("td:eq(3)").first()?.text()
         manga.description = infoElement.select("p.summary").first()?.text()
         manga.description = infoElement.select("p.summary").first()?.text()
         manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) }
         manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) }
         manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src")
         manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src")
+        return manga
     }
     }
 
 
     private fun parseStatus(status: String) = when {
     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 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 urlElement = element.select("a.tips").first()
 
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.name = urlElement.text()
         chapter.name = urlElement.text()
         chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
         chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
+        return chapter
     }
     }
 
 
     private fun parseChapterDate(date: String): Long {
     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 {
         document.select("select.m").first()?.select("option:not([value=0])")?.forEach {
             pages.add(Page(pages.size, "$url/${it.attr("value")}.html"))
             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 {
     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')
     // $('select.genres').map((i,el)=>`Genre("${$(el).next().text().trim()}", "${$(el).attr('name')}")`).get().join(',\n')
     // on http://mangafox.me/search.php
     // on http://mangafox.me/search.php
-    override fun getFilterList(): List<Filter<*>> = listOf(
+    override fun getFilterList() = FilterList(
             TextField("Author", "author"),
             TextField("Author", "author"),
             TextField("Artist", "artist"),
             TextField("Artist", "artist"),
             ListField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Japanese Manga", "1"), ListValue("Korean Manhwa", "2"), ListValue("Chinese Manhua", "3"))),
             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
 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.data.source.online.ParsedOnlineSource
 import okhttp3.HttpUrl
 import okhttp3.HttpUrl
+import okhttp3.Request
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
 import org.jsoup.nodes.Element
 import java.text.ParseException
 import java.text.ParseException
 import java.text.SimpleDateFormat
 import java.text.SimpleDateFormat
 import java.util.*
 import java.util.*
 
 
-class Mangahere(override val id: Int) : ParsedOnlineSource() {
+class Mangahere : ParsedOnlineSource() {
+
+    override val id: Long = 2
 
 
     override val name = "Mangahere"
     override val name = "Mangahere"
 
 
@@ -21,36 +23,42 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
 
 
     override val supportsLatest = true
     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 popularMangaSelector() = "div.directory_list > ul > li"
 
 
     override fun latestUpdatesSelector() = "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 {
         element.select(query).first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             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()
             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 popularMangaNextPageSelector() = "div.next-page > a.next"
 
 
     override fun latestUpdatesNextPageSelector() = "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)
         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) {
             when (filter) {
                 is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
                 is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
                 is Genre -> url.addQueryParameter(filter.id, filter.state.toString())
                 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")
                 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 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 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 detailElement = document.select(".manga_detail_top").first()
         val infoElement = detailElement.select(".detail_topText").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.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.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.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):")
         manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less")
         manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less")
         manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) }
         manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) }
         manga.thumbnail_url = detailElement.select("img.img").first()?.attr("src")
         manga.thumbnail_url = detailElement.select("img.img").first()?.attr("src")
+        return manga
     }
     }
 
 
     private fun parseStatus(status: String) = when {
     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 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 parentEl = element.select("span.left").first()
 
 
         val urlElement = parentEl.select("a").first()
         val urlElement = parentEl.select("a").first()
@@ -106,9 +116,11 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
             title = " - " + title
             title = " - " + title
         }
         }
 
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.name = urlElement.text() + volume + title
         chapter.name = urlElement.text() + volume + title
         chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0
         chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0
+        return chapter
     }
     }
 
 
     private fun parseChapterDate(date: String): Long {
     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 {
         document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
             pages.add(Page(pages.size, it.attr("value")))
             pages.add(Page(pages.size, it.attr("value")))
         }
         }
         pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
         pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
+        return pages
     }
     }
 
 
     override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
     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')
     // [...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
     // http://www.mangahere.co/advsearch.htm
-    override fun getFilterList(): List<Filter<*>> = listOf(
+    override fun getFilterList() = FilterList(
             TextField("Author", "author"),
             TextField("Author", "author"),
             TextField("Artist", "artist"),
             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"))),
             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
 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.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.data.source.online.ParsedOnlineSource
-import eu.kanade.tachiyomi.util.asJsoup
 import okhttp3.FormBody
 import okhttp3.FormBody
 import okhttp3.HttpUrl
 import okhttp3.HttpUrl
 import okhttp3.Request
 import okhttp3.Request
-import okhttp3.Response
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
 import org.jsoup.nodes.Element
 import java.text.SimpleDateFormat
 import java.text.SimpleDateFormat
 import java.util.regex.Pattern
 import java.util.regex.Pattern
 
 
-class Mangasee(override val id: Int) : ParsedOnlineSource() {
+class Mangasee : ParsedOnlineSource() {
+
+    override val id: Long = 9
 
 
     override val name = "Mangasee"
     override val name = "Mangasee"
 
 
@@ -30,46 +27,32 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
 
 
     private val indexPattern = Pattern.compile("-index-(.*?)-")
     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 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())
         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 {
         element.select("a.resultLink").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.text()
             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()
         val url = HttpUrl.parse("$baseUrl/search/request.php").newBuilder()
         if (!query.isEmpty()) url.addQueryParameter("keyword", query)
         if (!query.isEmpty()) url.addQueryParameter("keyword", query)
         var genres: String? = null
         var genres: String? = null
         var genresNo: 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) {
             when (filter) {
                 is Sort -> filter.values[filter.state].keys.forEachIndexed { i, s ->
                 is Sort -> filter.values[filter.state].keys.forEachIndexed { i, s ->
                     url.addQueryParameter(s, filter.values[filter.state].values[i])
                     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 (genres != null) url.addQueryParameter("genre", genres)
         if (genresNo != null) url.addQueryParameter("genreNo", genresNo)
         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())
         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) {
         for (i in 0..url.querySize() - 1) {
             body.add(url.queryParameterName(i), url.queryParameterValue(i))
             body.add(url.queryParameterName(i), url.queryParameterValue(i))
         }
         }
@@ -107,63 +82,57 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
         return Pair(body, requestUrl)
         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 {
         element.select("a.resultLink").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.text()
             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 detailElement = document.select("div.well > div.row").first()
 
 
+        val manga = SManga.create()
         manga.author = detailElement.select("a[href^=/search/?author=]").first()?.text()
         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.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.description = detailElement.select("strong:contains(Description:) + div").first()?.text()
         manga.status = detailElement.select("a[href^=/search/?status=]").first()?.text().orEmpty().let { parseStatus(it) }
         manga.status = detailElement.select("a[href^=/search/?status=]").first()?.text().orEmpty().let { parseStatus(it) }
         manga.thumbnail_url = detailElement.select("div > img").first()?.absUrl("src")
         manga.thumbnail_url = detailElement.select("div > img").first()?.absUrl("src")
+        return manga
     }
     }
 
 
     private fun parseStatus(status: String) = when {
     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 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 urlElement = element.select("a").first()
 
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.name = element.select("span.chapterLabel").first().text()?.let { it } ?: ""
         chapter.name = element.select("span.chapterLabel").first().text()?.let { it } ?: ""
         chapter.date_upload = element.select("time").first()?.attr("datetime")?.let { parseChapterDate(it) } ?: 0
         chapter.date_upload = element.select("time").first()?.attr("datetime")?.let { parseChapterDate(it) } ?: 0
+        return chapter
     }
     }
 
 
     private fun parseChapterDate(dateAsString: String): Long {
     private fun parseChapterDate(dateAsString: String): Long {
         return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(dateAsString).time
         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 url = fullUrl.substringBeforeLast('/')
 
 
+        val pages = mutableListOf<Page>()
+
         val series = document.select("input.IndexName").first().attr("value")
         val series = document.select("input.IndexName").first().attr("value")
         val chapter = document.select("span.CurChapter").first().text()
         val chapter = document.select("span.CurChapter").first().text()
         var index = ""
         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.add(Page(pages.size, "$url/$series-chapter-$chapter$index-page-${pages.size + 1}.html"))
         }
         }
         pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
         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")
     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')
     // [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
     // http://mangasee.co/advanced-search/
     // http://mangasee.co/advanced-search/
-    override fun getFilterList(): List<Filter<*>> = listOf(
+    override fun getFilterList() = FilterList(
             TextField("Years", "year"),
             TextField("Years", "year"),
             TextField("Author", "author"),
             TextField("Author", "author"),
             Sort("Sort By", arrayOf(SortOption("Alphabetical A-Z", emptyArray(), emptyArray()),
             Sort("Sort By", arrayOf(SortOption("Alphabetical A-Z", emptyArray(), emptyArray()),
@@ -249,34 +215,18 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
             Genre("Yuri")
             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 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())
         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 {
         element.select("a.latestSeries").first().let {
             val chapterUrl = it.attr("href")
             val chapterUrl = it.attr("href")
             val indexOfMangaUrl = chapterUrl.indexOf("-chapter-")
             val indexOfMangaUrl = chapterUrl.indexOf("-chapter-")
@@ -288,6 +238,7 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
             manga.setUrlWithoutDomain("/manga" + mangaUrl)
             manga.setUrlWithoutDomain("/manga" + mangaUrl)
             manga.title = title
             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
 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.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.data.source.online.ParsedOnlineSource
 import okhttp3.Headers
 import okhttp3.Headers
 import okhttp3.OkHttpClient
 import okhttp3.OkHttpClient
@@ -13,7 +11,9 @@ import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
 import org.jsoup.nodes.Element
 import java.util.*
 import java.util.*
 
 
-class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
+class Readmangatoday : ParsedOnlineSource() {
+
+    override val id: Long = 8
 
 
     override val name = "ReadMangaToday"
     override val name = "ReadMangaToday"
 
 
@@ -33,41 +33,39 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
         add("X-Requested-With", "XMLHttpRequest")
         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 popularMangaSelector() = "div.hot-manga > div.style-list > div.box"
 
 
     override fun latestUpdatesSelector() = "div.hot-manga > div.style-grid > 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 {
         element.select("div.title > h2 > a").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.attr("title")
             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 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()
         val builder = okhttp3.FormBody.Builder()
         builder.add("manga-name", query)
         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) {
             when (filter) {
                 is TextField -> builder.add(filter.key, filter.state)
                 is TextField -> builder.add(filter.key, filter.state)
                 is Type -> builder.add("type", arrayOf("all", "japanese", "korean", "chinese")[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) {
                 is Genre -> when (filter.state) {
                     Filter.TriState.STATE_INCLUDE -> builder.add("include[]", filter.id.toString())
                     Filter.TriState.STATE_INCLUDE -> builder.add("include[]", filter.id.toString())
                     Filter.TriState.STATE_EXCLUDE -> builder.add("exclude[]", 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 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 {
         element.select("div.title > h2 > a").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.attr("title")
             manga.title = it.attr("title")
         }
         }
+        return manga
     }
     }
 
 
     override fun searchMangaNextPageSelector() = "div.next-page > a.next"
     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 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.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.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.genre = detailElement.select("dl.dl-horizontal > dd:eq(5)").first()?.text()
         manga.description = detailElement.select("li.movie-detail").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.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")
         manga.thumbnail_url = detailElement.select("img.img-responsive").first()?.attr("src")
+        return manga
     }
     }
 
 
     private fun parseStatus(status: String) = when {
     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 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 urlElement = element.select("a").first()
 
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.name = urlElement.select("span.val").text()
         chapter.name = urlElement.select("span.val").text()
         chapter.date_upload = element.select("span.dte").first()?.text()?.let { parseChapterDate(it) } ?: 0
         chapter.date_upload = element.select("span.dte").first()?.text()?.let { parseChapterDate(it) } ?: 0
+        return chapter
     }
     }
 
 
     private fun parseChapterDate(date: String): Long {
     private fun parseChapterDate(date: String): Long {
@@ -125,7 +128,7 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
 
 
         if (dateWords.size == 3) {
         if (dateWords.size == 3) {
             val timeAgo = Integer.parseInt(dateWords[0])
             val timeAgo = Integer.parseInt(dateWords[0])
-            var date: Calendar = Calendar.getInstance()
+            val date: Calendar = Calendar.getInstance()
 
 
             if (dateWords[1].contains("Minute")) {
             if (dateWords[1].contains("Minute")) {
                 date.add(Calendar.MINUTE, -timeAgo)
                 date.add(Calendar.MINUTE, -timeAgo)
@@ -141,17 +144,19 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
                 date.add(Calendar.YEAR, -timeAgo)
                 date.add(Calendar.YEAR, -timeAgo)
             }
             }
 
 
-            return date.getTimeInMillis()
+            return date.timeInMillis
         }
         }
 
 
         return 0L
         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 {
         document.select("ul.list-switcher-2 > li > select.jump-menu").first().getElementsByTag("option").forEach {
             pages.add(Page(pages.size, it.attr("value")))
             pages.add(Page(pages.size, it.attr("value")))
         }
         }
         pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
         pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
+        return pages
     }
     }
 
 
     override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src")
     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')
     // [...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
     // http://www.readmanga.today/advanced-search
-    override fun getFilterList(): List<Filter<*>> = listOf(
+    override fun getFilterList() = FilterList(
             TextField("Author", "author-name"),
             TextField("Author", "author-name"),
             TextField("Artist", "artist-name"),
             TextField("Artist", "artist-name"),
             Type(),
             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
 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.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.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.Document
 import org.jsoup.nodes.Element
 import org.jsoup.nodes.Element
 import java.text.SimpleDateFormat
 import java.text.SimpleDateFormat
 
 
-class WieManga(override val id: Int) : ParsedOnlineSource() {
+class WieManga : ParsedOnlineSource() {
+
+    override val id: Long = 10
 
 
     override val name = "Wie Manga!"
     override val name = "Wie Manga!"
 
 
@@ -20,50 +23,61 @@ class WieManga(override val id: Int) : ParsedOnlineSource() {
 
 
     override val supportsLatest = true
     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 popularMangaSelector() = ".booklist td > div"
 
 
     override fun latestUpdatesSelector() = ".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 image = element.select("dt img")
         val title = element.select("dd a:first-child")
         val title = element.select("dd a:first-child")
 
 
+        val manga = SManga.create()
         manga.setUrlWithoutDomain(title.attr("href"))
         manga.setUrlWithoutDomain(title.attr("href"))
         manga.title = title.text()
         manga.title = title.text()
         manga.thumbnail_url = image.attr("src")
         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 popularMangaNextPageSelector() = null
 
 
     override fun latestUpdatesNextPageSelector() = 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 searchMangaSelector() = ".searchresult td > div"
 
 
-    override fun searchMangaFromElement(element: Element, manga: Manga) {
+    override fun searchMangaFromElement(element: Element): SManga {
         val image = element.select(".resultimg img")
         val image = element.select(".resultimg img")
         val title = element.select(".resultbookname")
         val title = element.select(".resultbookname")
 
 
+        val manga = SManga.create()
         manga.setUrlWithoutDomain(title.attr("href"))
         manga.setUrlWithoutDomain(title.attr("href"))
         manga.title = title.text()
         manga.title = title.text()
         manga.thumbnail_url = image.attr("src")
         manga.thumbnail_url = image.attr("src")
+        return manga
     }
     }
 
 
     override fun searchMangaNextPageSelector() = ".pagetor a.l"
     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 imageElement = document.select(".bookmessgae tr > td:nth-child(1)").first()
         val infoElement = document.select(".bookmessgae tr > td:nth-child(2)").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.author = infoElement.select("dd:nth-of-type(2) a").first()?.text()
         manga.artist = infoElement.select("dd:nth-of-type(3) 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", "")
         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")
         if (manga.artist == "RSS")
             manga.artist = null
             manga.artist = null
+        return manga
     }
     }
 
 
     override fun chapterListSelector() = ".chapterlist tr:not(:first-child)"
     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 urlElement = element.select(".col1 a").first()
         val dateElement = element.select(".col3 a").first()
         val dateElement = element.select(".col3 a").first()
 
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.name = urlElement.text()
         chapter.name = urlElement.text()
         chapter.date_upload = dateElement?.text()?.let { parseChapterDate(it) } ?: 0
         chapter.date_upload = dateElement?.text()?.let { parseChapterDate(it) } ?: 0
+        return chapter
     }
     }
 
 
     private fun parseChapterDate(date: String): Long {
     private fun parseChapterDate(date: String): Long {
         return SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(date).time
         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 {
         document.select("select#page").first().select("option").forEach {
             pages.add(Page(pages.size, it.attr("value")))
             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")
     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
 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.data.source.online.ParsedOnlineSource
 import eu.kanade.tachiyomi.util.asJsoup
 import eu.kanade.tachiyomi.util.asJsoup
+import okhttp3.Request
 import okhttp3.Response
 import okhttp3.Response
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
 import org.jsoup.nodes.Element
 import java.text.SimpleDateFormat
 import java.text.SimpleDateFormat
 import java.util.*
 import java.util.*
 
 
-class Mangachan(override val id: Int) : ParsedOnlineSource() {
+class Mangachan : ParsedOnlineSource() {
+
+    override val id: Long = 7
 
 
     override val name = "Mangachan"
     override val name = "Mangachan"
 
 
@@ -22,23 +23,28 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
 
 
     override val supportsLatest = true
     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 {
         } else {
-            val filt = filters.filter { it.state != Filter.TriState.STATE_IGNORE }
+            val filt = filters.filterIsInstance<Genre>().filter { !it.isIgnored() }
             if (filt.isNotEmpty()) {
             if (filt.isNotEmpty()) {
                 var genres = ""
                 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 {
             } 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"
     override fun popularMangaSelector() = "div.content_row"
@@ -47,22 +53,26 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
 
 
     override fun searchMangaSelector() = popularMangaSelector()
     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 {
         element.select("h2 > a").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.text()
             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 {
         element.select("a:nth-child(1)").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.text()
             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(Вперед)"
     override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
@@ -73,74 +83,80 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
 
 
     private fun searchGenresNextPageSelector() = popularMangaNextPageSelector()
     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()
         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 infoElement = document.select("table.mangatitle").first()
         val descElement = document.select("div#description").first()
         val descElement = document.select("div#description").first()
         val imgElement = document.select("img#cover").first()
         val imgElement = document.select("img#cover").first()
 
 
+        val manga = SManga.create()
         manga.author = infoElement.select("tr:eq(2) > td:eq(1)").text()
         manga.author = infoElement.select("tr:eq(2) > td:eq(1)").text()
         manga.genre = infoElement.select("tr:eq(5) > 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.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text())
         manga.description = descElement.textNodes().first().text()
         manga.description = descElement.textNodes().first().text()
         manga.thumbnail_url = baseUrl + imgElement.attr("src")
         manga.thumbnail_url = baseUrl + imgElement.attr("src")
+        return manga
     }
     }
 
 
     private fun parseStatus(element: String): Int {
     private fun parseStatus(element: String): Int {
         when {
         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 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 urlElement = element.select("a").first()
 
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.setUrlWithoutDomain(urlElement.attr("href"))
         chapter.name = urlElement.text()
         chapter.name = urlElement.text()
         chapter.date_upload = element.select("div.date").first()?.text()?.let {
         chapter.date_upload = element.select("div.date").first()?.text()?.let {
             SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it).time
             SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it).time
         } ?: 0
         } ?: 0
+        return chapter
     }
     }
 
 
-    override fun pageListParse(response: Response, pages: MutableList<Page>) {
+    override fun pageListParse(response: Response): List<Page> {
         val html = response.body().string()
         val html = response.body().string()
         val beginIndex = html.indexOf("fullimg\":[") + 10
         val beginIndex = html.indexOf("fullimg\":[") + 10
         val endIndex = html.indexOf(",]", beginIndex)
         val endIndex = html.indexOf(",]", beginIndex)
         val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "")
         val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "")
         val pageUrls = trimmedHtml.split(',')
         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) = ""
     override fun imageUrlParse(document: Document) = ""
@@ -152,7 +168,7 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
     *  return `Genre("${id.replace("_", " ")}")` }).join(',\n')
     *  return `Genre("${id.replace("_", " ")}")` }).join(',\n')
     *  on http://mangachan.me/
     *  on http://mangachan.me/
     */
     */
-    override fun getFilterList(): List<Filter<*>> = listOf(
+    override fun getFilterList() = FilterList(
             Genre("18 плюс"),
             Genre("18 плюс"),
             Genre("bdsm"),
             Genre("bdsm"),
             Genre("арт"),
             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
 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 eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
+import okhttp3.Request
 import okhttp3.Response
 import okhttp3.Response
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
 import org.jsoup.nodes.Element
@@ -11,7 +11,9 @@ import java.text.SimpleDateFormat
 import java.util.*
 import java.util.*
 import java.util.regex.Pattern
 import java.util.regex.Pattern
 
 
-class Mintmanga(override val id: Int) : ParsedOnlineSource() {
+class Mintmanga : ParsedOnlineSource() {
+
+    override val id: Long = 6
 
 
     override val name = "Mintmanga"
     override val name = "Mintmanga"
 
 
@@ -21,77 +23,89 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() {
 
 
     override val supportsLatest = true
     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 popularMangaSelector() = "div.desc"
 
 
     override fun latestUpdatesSelector() = "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 {
         element.select("h3 > a").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.attr("title")
             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 popularMangaNextPageSelector() = "a.nextLink"
 
 
     override fun latestUpdatesNextPageSelector() = "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 searchMangaSelector() = popularMangaSelector()
 
 
-    override fun searchMangaFromElement(element: Element, manga: Manga) {
-        popularMangaFromElement(element, manga)
+    override fun searchMangaFromElement(element: Element): SManga {
+        return popularMangaFromElement(element)
     }
     }
 
 
     // max 200 results
     // max 200 results
     override fun searchMangaNextPageSelector() = null
     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 infoElement = document.select("div.leftContent").first()
 
 
+        val manga = SManga.create()
         manga.author = infoElement.select("span.elem_author").first()?.text()
         manga.author = infoElement.select("span.elem_author").first()?.text()
         manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",")
         manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",")
         manga.description = infoElement.select("div.manga-description").text()
         manga.description = infoElement.select("div.manga-description").text()
         manga.status = parseStatus(infoElement.html())
         manga.status = parseStatus(infoElement.html())
         manga.thumbnail_url = infoElement.select("img").attr("data-full")
         manga.thumbnail_url = infoElement.select("img").attr("data-full")
+        return manga
     }
     }
 
 
     private fun parseStatus(element: String): Int {
     private fun parseStatus(element: String): Int {
         when {
         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 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 urlElement = element.select("a").first()
 
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1")
         chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1")
         chapter.name = urlElement.text().replace(" новое", "")
         chapter.name = urlElement.text().replace(" новое", "")
         chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
         chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
             SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
             SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
         } ?: 0
         } ?: 0
+        return chapter
     }
     }
 
 
-    override fun prepareNewChapter(chapter: Chapter, manga: Manga) {
+    override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
         chapter.chapter_number = -2f
         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 html = response.body().string()
         val beginIndex = html.indexOf("rm_h.init( [")
         val beginIndex = html.indexOf("rm_h.init( [")
         val endIndex = html.indexOf("], 0, false);", beginIndex)
         val endIndex = html.indexOf("], 0, false);", beginIndex)
@@ -100,14 +114,18 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() {
         val p = Pattern.compile("'.+?','.+?',\".+?\"")
         val p = Pattern.compile("'.+?','.+?',\".+?\"")
         val m = p.matcher(trimmedHtml)
         val m = p.matcher(trimmedHtml)
 
 
+        val pages = mutableListOf<Page>()
+
         var i = 0
         var i = 0
         while (m.find()) {
         while (m.find()) {
             val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
             val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
             pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
             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) = ""
     override fun imageUrlParse(document: Document) = ""
@@ -119,7 +137,7 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() {
     *  return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
     *  return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
     *  on http://mintmanga.com/search
     *  on http://mintmanga.com/search
     */
     */
-    override fun getFilterList(): List<Filter<*>> = listOf(
+    override fun getFilterList() = FilterList(
             Genre("арт", "el_2220"),
             Genre("арт", "el_2220"),
             Genre("бара", "el_1353"),
             Genre("бара", "el_1353"),
             Genre("боевик", "el_1346"),
             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
 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 eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
+import okhttp3.Request
 import okhttp3.Response
 import okhttp3.Response
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
 import org.jsoup.nodes.Element
@@ -11,7 +11,9 @@ import java.text.SimpleDateFormat
 import java.util.*
 import java.util.*
 import java.util.regex.Pattern
 import java.util.regex.Pattern
 
 
-class Readmanga(override val id: Int) : ParsedOnlineSource() {
+class Readmanga : ParsedOnlineSource() {
+
+    override val id: Long = 5
 
 
     override val name = "Readmanga"
     override val name = "Readmanga"
 
 
@@ -21,77 +23,89 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() {
 
 
     override val supportsLatest = true
     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 popularMangaSelector() = "div.desc"
 
 
     override fun latestUpdatesSelector() = "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 {
         element.select("h3 > a").first().let {
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.setUrlWithoutDomain(it.attr("href"))
             manga.title = it.attr("title")
             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 popularMangaNextPageSelector() = "a.nextLink"
 
 
     override fun latestUpdatesNextPageSelector() = "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 searchMangaSelector() = popularMangaSelector()
 
 
-    override fun searchMangaFromElement(element: Element, manga: Manga) {
-        popularMangaFromElement(element, manga)
+    override fun searchMangaFromElement(element: Element): SManga {
+        return popularMangaFromElement(element)
     }
     }
 
 
     // max 200 results
     // max 200 results
     override fun searchMangaNextPageSelector() = null
     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 infoElement = document.select("div.leftContent").first()
 
 
+        val manga = SManga.create()
         manga.author = infoElement.select("span.elem_author").first()?.text()
         manga.author = infoElement.select("span.elem_author").first()?.text()
         manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",")
         manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",")
         manga.description = infoElement.select("div.manga-description").text()
         manga.description = infoElement.select("div.manga-description").text()
         manga.status = parseStatus(infoElement.html())
         manga.status = parseStatus(infoElement.html())
         manga.thumbnail_url = infoElement.select("img").attr("data-full")
         manga.thumbnail_url = infoElement.select("img").attr("data-full")
+        return manga
     }
     }
 
 
     private fun parseStatus(element: String): Int {
     private fun parseStatus(element: String): Int {
         when {
         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 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 urlElement = element.select("a").first()
 
 
+        val chapter = SChapter.create()
         chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1")
         chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1")
         chapter.name = urlElement.text().replace(" новое", "")
         chapter.name = urlElement.text().replace(" новое", "")
         chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
         chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
             SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
             SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
         } ?: 0
         } ?: 0
+        return chapter
     }
     }
 
 
-    override fun prepareNewChapter(chapter: Chapter, manga: Manga) {
+    override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
         chapter.chapter_number = -2f
         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 html = response.body().string()
         val beginIndex = html.indexOf("rm_h.init( [")
         val beginIndex = html.indexOf("rm_h.init( [")
         val endIndex = html.indexOf("], 0, false);", beginIndex)
         val endIndex = html.indexOf("], 0, false);", beginIndex)
@@ -100,14 +114,18 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() {
         val p = Pattern.compile("'.+?','.+?',\".+?\"")
         val p = Pattern.compile("'.+?','.+?',\".+?\"")
         val m = p.matcher(trimmedHtml)
         val m = p.matcher(trimmedHtml)
 
 
+        val pages = mutableListOf<Page>()
+
         var i = 0
         var i = 0
         while (m.find()) {
         while (m.find()) {
             val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
             val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
             pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
             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) = ""
     override fun imageUrlParse(document: Document) = ""
@@ -119,7 +137,7 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() {
     *  return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
     *  return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
     *  on http://readmanga.me/search
     *  on http://readmanga.me/search
     */
     */
-    override fun getFilterList(): List<Filter<*>> = listOf(
+    override fun getFilterList() = FilterList(
             Genre("арт", "el_5685"),
             Genre("арт", "el_5685"),
             Genre("боевик", "el_2155"),
             Genre("боевик", "el_2155"),
             Genre("боевые искусства", "el_2143"),
             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 com.f2prateek.rx.preferences.Preference
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.source.model.FilterList
 import eu.kanade.tachiyomi.data.source.online.LoginSource
 import eu.kanade.tachiyomi.data.source.online.LoginSource
 import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
 import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
 import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
 import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
@@ -32,7 +33,6 @@ import nucleus.factory.RequiresPresenter
 import rx.Subscription
 import rx.Subscription
 import rx.android.schedulers.AndroidSchedulers
 import rx.android.schedulers.AndroidSchedulers
 import rx.subjects.PublishSubject
 import rx.subjects.PublishSubject
-import timber.log.Timber
 import java.util.concurrent.TimeUnit.MILLISECONDS
 import java.util.concurrent.TimeUnit.MILLISECONDS
 
 
 /**
 /**
@@ -104,6 +104,11 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
     private val toolbar: Toolbar
     private val toolbar: Toolbar
         get() = (activity as MainActivity).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.
      * Navigation view containing filter items.
      */
      */
@@ -201,8 +206,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
             } else if (source != presenter.source) {
             } else if (source != presenter.source) {
                 selectedIndex = position
                 selectedIndex = position
                 showProgressBar()
                 showProgressBar()
-                glm.scrollToPositionWithOffset(0, 0)
-                llm.scrollToPositionWithOffset(0, 0)
+                adapter.clear()
                 presenter.setActiveSource(source)
                 presenter.setActiveSource(source)
                 navView?.setFilters(presenter.sourceFilters)
                 navView?.setFilters(presenter.sourceFilters)
                 activity.invalidateOptionsMenu()
                 activity.invalidateOptionsMenu()
@@ -233,14 +237,14 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
         }
         }
 
 
         navView.onSearchClicked = {
         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 = {
         navView.onResetClicked = {
-            presenter.appliedFilters = emptyList()
+            presenter.appliedFilters = FilterList()
             val newFilters = presenter.source.getFilterList()
             val newFilters = presenter.source.getFilterList()
             presenter.sourceFilters = newFilters
             presenter.sourceFilters = newFilters
             navView.setFilters(newFilters)
             navView.setFilters(newFilters)
@@ -277,7 +281,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
         // Setup filters button
         // Setup filters button
         menu.findItem(R.id.action_set_filter).apply {
         menu.findItem(R.id.action_set_filter).apply {
             icon.mutate()
             icon.mutate()
-            if (presenter.source.filters.isEmpty()) {
+            if (presenter.sourceFilters.isEmpty()) {
                 isEnabled = false
                 isEnabled = false
                 icon.alpha = 128
                 icon.alpha = 128
             } else {
             } else {
@@ -355,8 +359,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
             return
             return
 
 
         showProgressBar()
         showProgressBar()
-        catalogue_grid.layoutManager.scrollToPosition(0)
-        catalogue_list.layoutManager.scrollToPosition(0)
+        adapter.clear()
 
 
         presenter.restartPager(newQuery)
         presenter.restartPager(newQuery)
     }
     }
@@ -394,9 +397,11 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
      */
      */
     fun onAddPageError(error: Throwable) {
     fun onAddPageError(error: Throwable) {
         hideProgressBar()
         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) {
             setAction(R.string.action_retry) {
                 showProgressBar()
                 showProgressBar()
                 presenter.requestNext()
                 presenter.requestNext()
@@ -456,6 +461,8 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
      */
      */
     private fun showProgressBar() {
     private fun showProgressBar() {
         progress.visibility = ProgressBar.VISIBLE
         progress.visibility = ProgressBar.VISIBLE
+        snack?.dismiss()
+        snack = null
     }
     }
 
 
     /**
     /**
@@ -463,6 +470,8 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
      */
      */
     private fun showGridProgressBar() {
     private fun showGridProgressBar() {
         progress_grid.visibility = ProgressBar.VISIBLE
         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.ArrayAdapter
 import android.widget.TextView
 import android.widget.TextView
 import eu.kanade.tachiyomi.R
 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.dpToPx
 import eu.kanade.tachiyomi.util.getResourceColor
 import eu.kanade.tachiyomi.util.getResourceColor
 import eu.kanade.tachiyomi.util.inflate
 import eu.kanade.tachiyomi.util.inflate
@@ -38,14 +39,14 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
         reset_btn.setOnClickListener { onResetClicked() }
         reset_btn.setOnClickListener { onResetClicked() }
     }
     }
 
 
-    fun setFilters(items: List<Filter<*>>) {
+    fun setFilters(items: FilterList) {
         adapter.items = items
         adapter.items = items
         adapter.notifyDataSetChanged()
         adapter.notifyDataSetChanged()
     }
     }
 
 
     inner class Adapter : RecyclerView.Adapter<Holder>() {
     inner class Adapter : RecyclerView.Adapter<Holder>() {
 
 
-        var items: List<Filter<*>> = emptyList()
+        var items: FilterList = FilterList()
 
 
         override fun getItemCount(): Int {
         override fun getItemCount(): Int {
             return items.size
             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
 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.model.MangasPage
-import eu.kanade.tachiyomi.data.source.online.OnlineSource
-import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
 import rx.Observable
 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())
         val observable = if (query.isBlank() && filters.isEmpty())
             source.fetchPopularManga(page)
             source.fetchPopularManga(page)
         else
         else
             source.fetchSearchManga(page, query, filters)
             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.database.models.Manga
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.data.preference.getOrDefault
 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.Source
 import eu.kanade.tachiyomi.data.source.SourceManager
 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.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 eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 import rx.Observable
 import rx.Observable
 import rx.Subscription
 import rx.Subscription
@@ -55,7 +55,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
     /**
     /**
      * Active source.
      * Active source.
      */
      */
-    lateinit var source: OnlineSource
+    lateinit var source: CatalogueSource
         private set
         private set
 
 
     /**
     /**
@@ -67,12 +67,12 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
     /**
     /**
      * Modifiable list of filters.
      * 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.
      * 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.
      * Pager containing a list of manga results.
@@ -136,7 +136,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
      * @param query the query.
      * @param query the query.
      * @param filters the current state of the filters (for search mode).
      * @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.query = query
         this.appliedFilters = filters
         this.appliedFilters = filters
 
 
@@ -145,11 +145,17 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
         // Create a new pager.
         // Create a new pager.
         pager = createPager(query, filters)
         pager = createPager(query, filters)
 
 
+        val sourceId = source.id
+
         // Prepare the pager.
         // Prepare the pager.
         pagerSubscription?.let { remove(it) }
         pagerSubscription?.let { remove(it) }
         pagerSubscription = pager.results()
         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 ->
                 }, { view, error ->
                     Timber.e(error)
                     Timber.e(error)
                 })
                 })
@@ -165,7 +171,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
         if (!hasNextPage()) return
         if (!hasNextPage()) return
 
 
         pageSubscription?.let { remove(it) }
         pageSubscription?.let { remove(it) }
-        pageSubscription = pager.requestNext { getPageTransformer(it) }
+        pageSubscription = Observable.defer { pager.requestNext() }
                 .subscribeFirst({ view, page ->
                 .subscribeFirst({ view, page ->
                     // Nothing to do when onNext is emitted.
                     // Nothing to do when onNext is emitted.
                 }, CatalogueFragment::onAddPageError)
                 }, CatalogueFragment::onAddPageError)
@@ -175,7 +181,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
      * Returns true if the last fetched page has a next page.
      * Returns true if the last fetched page has a next page.
      */
      */
     fun hasNextPage(): Boolean {
     fun hasNextPage(): Boolean {
-        return pager.hasNextPage()
+        return pager.hasNextPage
     }
     }
 
 
     /**
     /**
@@ -183,12 +189,12 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
      *
      *
      * @param source the new active source.
      * @param source the new active source.
      */
      */
-    fun setActiveSource(source: OnlineSource) {
+    fun setActiveSource(source: CatalogueSource) {
         prefs.lastUsedCatalogueSource().set(source.id)
         prefs.lastUsedCatalogueSource().set(source.id)
         this.source = source
         this.source = source
         sourceFilters = source.getFilterList()
         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?.let { remove(it) }
         initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io())
         initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io())
                 .flatMap { Observable.from(it) }
                 .flatMap { Observable.from(it) }
-                .filter { !it.initialized }
+                .filter { it.thumbnail_url == null && !it.initialized }
                 .concatMap { getMangaDetailsObservable(it) }
                 .concatMap { getMangaDetailsObservable(it) }
                 .onBackpressureBuffer()
                 .onBackpressureBuffer()
                 .observeOn(AndroidSchedulers.mainThread())
                 .observeOn(AndroidSchedulers.mainThread())
@@ -221,41 +227,21 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
                 .apply { add(this) }
                 .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
      * 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.
      * 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.
      * @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) {
         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
         return localManga
     }
     }
@@ -279,6 +265,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
         return source.fetchMangaDetails(manga)
         return source.fetchMangaDetails(manga)
                 .flatMap { networkManga ->
                 .flatMap { networkManga ->
                     manga.copyFrom(networkManga)
                     manga.copyFrom(networkManga)
+                    manga.initialized = true
                     db.insertManga(manga).executeAsBlocking()
                     db.insertManga(manga).executeAsBlocking()
                     Observable.just(manga)
                     Observable.just(manga)
                 }
                 }
@@ -290,13 +277,13 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
      *
      *
      * @return a source.
      * @return a source.
      */
      */
-    fun getLastUsedSource(): OnlineSource {
+    fun getLastUsedSource(): CatalogueSource {
         val id = prefs.lastUsedCatalogueSource().get() ?: -1
         val id = prefs.lastUsedCatalogueSource().get() ?: -1
         val source = sourceManager.get(id)
         val source = sourceManager.get(id)
         if (!isValidSource(source)) {
         if (!isValidSource(source)) {
             return findFirstValidSource()
             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.
      * @return the index of the first valid source.
      */
      */
-    fun findFirstValidSource(): OnlineSource {
+    fun findFirstValidSource(): CatalogueSource {
         return sources.first { isValidSource(it) }
         return sources.first { isValidSource(it) }
     }
     }
 
 
     /**
     /**
      * Returns a list of enabled sources ordered by language and name.
      * 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 languages = prefs.enabledLanguages().getOrDefault()
         val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault()
         val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault()
 
 
@@ -336,7 +323,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
             languages.add("en")
             languages.add("en")
         }
         }
 
 
-        return sourceManager.getOnlineSources()
+        return sourceManager.getCatalogueSources()
                 .filter { it.lang in languages }
                 .filter { it.lang in languages }
                 .filterNot { it.id.toString() in hiddenCatalogues }
                 .filterNot { it.id.toString() in hiddenCatalogues }
                 .sortedBy { "(${it.lang}) ${it.name}" }
                 .sortedBy { "(${it.lang}) ${it.name}" }
@@ -365,13 +352,13 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
     /**
     /**
      * Set the filter states for the current source.
      * 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)
         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)
         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
 package eu.kanade.tachiyomi.ui.catalogue
 
 
+import com.jakewharton.rxrelay.PublishRelay
 import eu.kanade.tachiyomi.data.source.model.MangasPage
 import eu.kanade.tachiyomi.data.source.model.MangasPage
-import rx.subjects.PublishSubject
+import eu.kanade.tachiyomi.data.source.model.SManga
 import rx.Observable
 import rx.Observable
 
 
 /**
 /**
  * A general pager for source requests (latest updates, popular, search)
  * 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()
         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
 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.model.MangasPage
-import eu.kanade.tachiyomi.data.source.online.OnlineSource
 import eu.kanade.tachiyomi.ui.catalogue.Pager
 import eu.kanade.tachiyomi.ui.catalogue.Pager
 import rx.Observable
 import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
 
 
 /**
 /**
  * LatestUpdatesPager inherited from the general Pager.
  * 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
 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.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.CataloguePresenter
 import eu.kanade.tachiyomi.ui.catalogue.Pager
 import eu.kanade.tachiyomi.ui.catalogue.Pager
-import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
 
 
 /**
 /**
  * Presenter of [LatestUpdatesFragment]. Inherit CataloguePresenter.
  * Presenter of [LatestUpdatesFragment]. Inherit CataloguePresenter.
  */
  */
 class LatestUpdatesPresenter : CataloguePresenter() {
 class LatestUpdatesPresenter : CataloguePresenter() {
 
 
-    override fun createPager(query: String, filters: List<Filter<*>>): Pager {
+    override fun createPager(query: String, filters: FilterList): Pager {
         return LatestUpdatesPager(source)
         return LatestUpdatesPager(source)
     }
     }
 
 
-    override fun getEnabledSources(): List<OnlineSource> {
+    override fun getEnabledSources(): List<CatalogueSource> {
         return super.getEnabledSources().filter { it.supportsLatest }
         return super.getEnabledSources().filter { it.supportsLatest }
     }
     }
 
 
     override fun isValidSource(source: Source?): Boolean {
     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>> {
     private fun applyFilters(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
         // Cached list of downloaded manga directories given a source id.
         // 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.
         // Cached list of downloaded chapter directories for a manga.
         val chapterDirectories = mutableMapOf<Long, Boolean>()
         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.
      * 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())
             .subscribeOn(Schedulers.io())
             .map { syncChaptersWithSource(db, it, manga, source) }
             .map { syncChaptersWithSource(db, it, manga, source) }
             .observeOn(AndroidSchedulers.mainThread())
             .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.R
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.source.Source
 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.data.source.online.OnlineSource
 import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
 import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
 import eu.kanade.tachiyomi.ui.manga.MangaActivity
 import eu.kanade.tachiyomi.ui.manga.MangaActivity
@@ -122,9 +123,9 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
 
 
         // Update status TextView.
         // Update status TextView.
         manga_status.setText(when (manga.status) {
         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
             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.
      * @return manga information.
      */
      */
     private fun fetchMangaObs(): Observable<Manga> {
     private fun fetchMangaObs(): Observable<Manga> {
-        return source.fetchMangaDetails(manga)
+        return Observable.defer { source.fetchMangaDetails(manga) }
                 .flatMap { networkManga ->
                 .flatMap { networkManga ->
                     manga.copyFrom(networkManga)
                     manga.copyFrom(networkManga)
+                    manga.initialized = true
                     db.insertManga(manga).executeAsBlocking()
                     db.insertManga(manga).executeAsBlocking()
                     Observable.just<Manga>(manga)
                     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.download.DownloadManager
 import eu.kanade.tachiyomi.data.source.Source
 import eu.kanade.tachiyomi.data.source.Source
 import eu.kanade.tachiyomi.data.source.model.Page
 import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.data.source.online.OnlineSource
+import eu.kanade.tachiyomi.data.source.online.fetchImageFromCacheThenNet
+import eu.kanade.tachiyomi.data.source.online.fetchPageListFromCacheThenNet
 import eu.kanade.tachiyomi.util.plusAssign
 import eu.kanade.tachiyomi.util.plusAssign
 import rx.Observable
 import rx.Observable
 import rx.schedulers.Schedulers
 import rx.schedulers.Schedulers
@@ -36,9 +39,11 @@ class ChapterLoader(
     }
     }
 
 
     private fun prepareOnlineReading() {
     private fun prepareOnlineReading() {
+        if (source !is OnlineSource) return
+
         subscriptions += Observable.defer { Observable.just(queue.take().page) }
         subscriptions += Observable.defer { Observable.just(queue.take().page) }
                 .filter { it.status == Page.QUEUE }
                 .filter { it.status == Page.QUEUE }
-                .concatMap { source.fetchImage(it) }
+                .concatMap { source.fetchImageFromCacheThenNet(it) }
                 .repeat()
                 .repeat()
                 .subscribeOn(Schedulers.io())
                 .subscribeOn(Schedulers.io())
                 .subscribe({
                 .subscribe({
@@ -57,6 +62,10 @@ class ChapterLoader(
                     Observable.just(chapter.pages!!)
                     Observable.just(chapter.pages!!)
             }
             }
             .doOnNext { 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
                 // Now that the number of pages is known, fix the requested page if the last one
                 // was requested.
                 // was requested.
                 if (chapter.requestedPage == -1) {
                 if (chapter.requestedPage == -1) {
@@ -76,8 +85,8 @@ class ChapterLoader(
                     // Fetch the page list from disk.
                     // Fetch the page list from disk.
                     downloadManager.buildPageList(source, manga, chapter)
                     downloadManager.buildPageList(source, manga, chapter)
                 } else {
                 } else {
-                    // Fetch the page list from cache or fallback to network
-                    source.fetchPageList(chapter)
+                    (source as? OnlineSource)?.fetchPageListFromCacheThenNet(chapter)
+                            ?: source.fetchPageList(chapter)
                 }
                 }
             }
             }
             .doOnNext { pages ->
             .doOnNext { pages ->
@@ -111,6 +120,8 @@ class ChapterLoader(
         queue.offer(PriorityPage(page, 2))
         queue.offer(PriorityPage(page, 2))
     }
     }
 
 
+
+
     private data class PriorityPage(val page: Page, val priority: Int): Comparable<PriorityPage> {
     private data class PriorityPage(val page: Page, val priority: Int): Comparable<PriorityPage> {
 
 
         companion object {
         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 {
         Observable.fromCallable {
             // Cache current page list progress for online chapters to allow a faster reopen
             // Cache current page list progress for online chapters to allow a faster reopen
             if (!chapter.isDownloaded) {
             if (!chapter.isDownloaded) {
-                source.let { if (it is OnlineSource) it.savePageList(chapter, pages) }
+                source.let {
+                    if (it is OnlineSource) chapterCache.putPageListToCache(chapter, pages)
+                }
             }
             }
 
 
             try {
             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>) {
     private fun setDownloadedChapters(chapters: List<RecentChapter>) {
         // Cached list of downloaded manga directories.
         // 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.
         // Cached list of downloaded chapter directories for a manga.
         val chapterDirectories = mutableMapOf<Long, Array<UniFile>>()
         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?) {
     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()
             pref?.notifyChanged()
         }
         }
     }
     }
 
 
-    private fun getSourceKey(sourceId: Int): String {
+    private fun getSourceKey(sourceId: Long): String {
         return "source_$sourceId"
         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?) {
     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.Chapter
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.source.Source
 import eu.kanade.tachiyomi.data.source.Source
+import eu.kanade.tachiyomi.data.source.model.SChapter
 import eu.kanade.tachiyomi.data.source.online.OnlineSource
 import eu.kanade.tachiyomi.data.source.online.OnlineSource
 import java.util.*
 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.
  * Helper method for syncing the list of chapters from the source with the ones from the database.
  *
  *
  * @param db 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 manga the manga of the chapters.
  * @param source the source of the chapters.
  * @param source the source of the chapters.
  * @return a pair of new insertions and deletions.
  * @return a pair of new insertions and deletions.
  */
  */
 fun syncChaptersWithSource(db: DatabaseHelper,
 fun syncChaptersWithSource(db: DatabaseHelper,
-                           sourceChapters: List<Chapter>,
+                           rawSourceChapters: List<SChapter>,
                            manga: Manga,
                            manga: Manga,
                            source: Source) : Pair<List<Chapter>, List<Chapter>> {
                            source: Source) : Pair<List<Chapter>, List<Chapter>> {
 
 
+    if (rawSourceChapters.isEmpty()) {
+        throw Exception("No chapters found")
+    }
+
     // Chapters from db.
     // Chapters from db.
     val dbChapters = db.getChapters(manga).executeAsBlocking()
     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.
     // 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 length the duration of the snack.
  * @param f a function to execute in the snack, allowing for example to define a custom action.
  * @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 snack = Snackbar.make(this, message, length)
     val textView = snack.view.findViewById(android.support.design.R.id.snackbar_text) as TextView
     val textView = snack.view.findViewById(android.support.design.R.id.snackbar_text) as TextView
     textView.setTextColor(Color.WHITE)
     textView.setTextColor(Color.WHITE)
     snack.f()
     snack.f()
     snack.show()
     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
 package eu.kanade.tachiyomi.widget.preference
 
 
+import android.app.Activity
 import android.app.Dialog
 import android.app.Dialog
 import android.content.DialogInterface
 import android.content.DialogInterface
 import android.content.Intent
 import android.content.Intent
@@ -70,7 +71,8 @@ abstract class LoginDialogPreference : DialogFragment() {
 
 
     override fun onDismiss(dialog: DialogInterface) {
     override fun onDismiss(dialog: DialogInterface) {
         super.onDismiss(dialog)
         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()
     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 {
         fun newInstance(source: Source): LoginDialogPreference {
             val fragment = SourceLoginDialog()
             val fragment = SourceLoginDialog()
             val bundle = Bundle(1)
             val bundle = Bundle(1)
-            bundle.putInt("key", source.id)
+            bundle.putLong("key", source.id)
             fragment.arguments = bundle
             fragment.arguments = bundle
             return fragment
             return fragment
         }
         }
@@ -32,7 +32,7 @@ class SourceLoginDialog : LoginDialogPreference() {
     override fun onCreate(savedInstanceState: Bundle?) {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         super.onCreate(savedInstanceState)
 
 
-        val sourceId = arguments.getInt("key")
+        val sourceId = arguments.getLong("key")
         source = sourceManager.get(sourceId) as LoginSource
         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_enable_automatic_updates_key">automatic_updates</string>
 
 
     <string name="pref_display_catalogue_as_list">pref_display_catalogue_as_list</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>
     <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.Chapter
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.source.SourceManager
 import eu.kanade.tachiyomi.data.source.SourceManager
+import eu.kanade.tachiyomi.data.source.model.SChapter
 import eu.kanade.tachiyomi.data.source.online.OnlineSource
 import eu.kanade.tachiyomi.data.source.online.OnlineSource
 import org.assertj.core.api.Assertions.assertThat
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.Before
 import org.junit.Before
 import org.junit.Test
 import org.junit.Test
 import org.junit.runner.RunWith
 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.mockito.Mockito.*
 import org.mockito.Mockito.*
 import org.robolectric.Robolectric
 import org.robolectric.Robolectric
@@ -51,7 +52,7 @@ class LibraryUpdateServiceTest {
 
 
         service = Robolectric.setupService(LibraryUpdateService::class.java)
         service = Robolectric.setupService(LibraryUpdateService::class.java)
         source = mock(OnlineSource::class.java)
         source = mock(OnlineSource::class.java)
-        `when`(service.sourceManager.get(anyInt())).thenReturn(source)
+        `when`(service.sourceManager.get(anyLong())).thenReturn(source)
     }
     }
 
 
     @Test
     @Test
@@ -91,7 +92,7 @@ class LibraryUpdateServiceTest {
 
 
         // One of the updates will fail
         // One of the updates will fail
         `when`(source.fetchChapterList(favManga[0])).thenReturn(Observable.just(chapters))
         `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))
         `when`(source.fetchChapterList(favManga[2])).thenReturn(Observable.just(chapters3))
 
 
         val intent = Intent()
         val intent = Intent()
@@ -117,8 +118,7 @@ class LibraryUpdateServiceTest {
     private fun createManga(vararg urls: String): List<Manga> {
     private fun createManga(vararg urls: String): List<Manga> {
         val list = ArrayList<Manga>()
         val list = ArrayList<Manga>()
         for (url in urls) {
         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
             m.favorite = true
             list.add(m)
             list.add(m)
         }
         }